flask-appbuilder 3.2.1rc1__py3-none-any.whl → 5.0.2rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flask_appbuilder/__init__.py +2 -3
- flask_appbuilder/_compat.py +0 -1
- flask_appbuilder/actions.py +14 -14
- flask_appbuilder/api/__init__.py +741 -527
- flask_appbuilder/api/convert.py +104 -98
- flask_appbuilder/api/manager.py +14 -8
- flask_appbuilder/api/schemas.py +12 -1
- flask_appbuilder/babel/manager.py +12 -16
- flask_appbuilder/base.py +353 -280
- flask_appbuilder/basemanager.py +1 -1
- flask_appbuilder/baseviews.py +241 -164
- flask_appbuilder/charts/jsontools.py +10 -10
- flask_appbuilder/charts/views.py +56 -60
- flask_appbuilder/cli.py +115 -70
- flask_appbuilder/const.py +52 -52
- flask_appbuilder/exceptions.py +67 -5
- flask_appbuilder/fields.py +32 -23
- flask_appbuilder/fieldwidgets.py +34 -27
- flask_appbuilder/filemanager.py +33 -45
- flask_appbuilder/filters.py +11 -13
- flask_appbuilder/forms.py +31 -35
- flask_appbuilder/hooks.py +90 -0
- flask_appbuilder/menu.py +35 -10
- flask_appbuilder/models/base.py +47 -57
- flask_appbuilder/models/decorators.py +13 -13
- flask_appbuilder/models/filters.py +42 -38
- flask_appbuilder/models/generic/__init__.py +29 -29
- flask_appbuilder/models/generic/filters.py +11 -3
- flask_appbuilder/models/generic/interface.py +1 -3
- flask_appbuilder/models/group.py +37 -39
- flask_appbuilder/models/mixins.py +22 -18
- flask_appbuilder/models/sqla/__init__.py +19 -72
- flask_appbuilder/models/sqla/base.py +24 -0
- flask_appbuilder/models/sqla/base_legacy.py +132 -0
- flask_appbuilder/models/sqla/filters.py +132 -19
- flask_appbuilder/models/sqla/interface.py +390 -276
- flask_appbuilder/security/api.py +31 -35
- flask_appbuilder/security/decorators.py +181 -83
- flask_appbuilder/security/forms.py +20 -31
- flask_appbuilder/security/manager.py +715 -489
- flask_appbuilder/security/registerviews.py +29 -112
- flask_appbuilder/security/schemas.py +43 -0
- flask_appbuilder/security/sqla/apis/__init__.py +8 -0
- flask_appbuilder/security/sqla/apis/group/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/group/api.py +227 -0
- flask_appbuilder/security/sqla/apis/group/schema.py +73 -0
- flask_appbuilder/security/sqla/apis/permission/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/permission/api.py +19 -0
- flask_appbuilder/security/sqla/apis/permission_view_menu/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/permission_view_menu/api.py +16 -0
- flask_appbuilder/security/sqla/apis/role/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/role/api.py +306 -0
- flask_appbuilder/security/sqla/apis/role/schema.py +27 -0
- flask_appbuilder/security/sqla/apis/user/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/user/api.py +292 -0
- flask_appbuilder/security/sqla/apis/user/schema.py +97 -0
- flask_appbuilder/security/sqla/apis/user/validator.py +27 -0
- flask_appbuilder/security/sqla/apis/view_menu/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/view_menu/api.py +18 -0
- flask_appbuilder/security/sqla/manager.py +421 -203
- flask_appbuilder/security/sqla/models.py +192 -57
- flask_appbuilder/security/utils.py +9 -0
- flask_appbuilder/security/views.py +232 -229
- flask_appbuilder/static/.DS_Store +0 -0
- flask_appbuilder/static/appbuilder/css/ab.css +20 -12
- flask_appbuilder/static/appbuilder/css/bootstrap-datepicker/bootstrap-datepicker3.min.css +7 -0
- flask_appbuilder/static/appbuilder/css/bootstrap.min.css.map +1 -0
- flask_appbuilder/static/appbuilder/css/flags/flags16.css +249 -245
- flask_appbuilder/static/appbuilder/css/fontawesome/all.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/brands.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/fontawesome.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/regular.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/solid.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/svg-with-js.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v4-font-face.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v4-shims.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v5-font-face.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/images/flags16.png +0 -0
- flask_appbuilder/static/appbuilder/css/select2/select2-bootstrap.min.css +7 -0
- flask_appbuilder/static/appbuilder/css/select2/select2.min.css +1 -0
- flask_appbuilder/static/appbuilder/css/swagger/swagger-ui.css +3 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.woff2 +0 -0
- flask_appbuilder/static/appbuilder/js/ab.js +33 -23
- flask_appbuilder/static/appbuilder/js/ab_filters.js +91 -84
- flask_appbuilder/static/appbuilder/js/bootstrap-datepicker/bootstrap-datepicker.min.js +8 -0
- flask_appbuilder/static/appbuilder/js/jquery-latest.js +2 -2
- flask_appbuilder/static/appbuilder/js/select2/select2.min.js +2 -0
- flask_appbuilder/static/appbuilder/js/swagger-ui-bundle.js +3 -0
- flask_appbuilder/templates/appbuilder/baselib.html +9 -3
- flask_appbuilder/templates/appbuilder/general/lib.html +60 -34
- flask_appbuilder/templates/appbuilder/general/model/edit.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/edit_cascade.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/search.html +3 -2
- flask_appbuilder/templates/appbuilder/general/model/show.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/show_cascade.html +1 -1
- flask_appbuilder/templates/appbuilder/general/security/login_db.html +7 -7
- flask_appbuilder/templates/appbuilder/general/security/login_ldap.html +5 -5
- flask_appbuilder/templates/appbuilder/general/security/login_oauth.html +24 -49
- flask_appbuilder/templates/appbuilder/general/widgets/base_list.html +2 -1
- flask_appbuilder/templates/appbuilder/general/widgets/chart.html +4 -2
- flask_appbuilder/templates/appbuilder/general/widgets/direct_chart.html +4 -3
- flask_appbuilder/templates/appbuilder/general/widgets/multiple_chart.html +3 -2
- flask_appbuilder/templates/appbuilder/general/widgets/search.html +11 -10
- flask_appbuilder/templates/appbuilder/init.html +37 -43
- flask_appbuilder/templates/appbuilder/navbar_menu.html +1 -1
- flask_appbuilder/templates/appbuilder/navbar_right.html +2 -2
- flask_appbuilder/templates/appbuilder/swagger/swagger.html +22 -19
- flask_appbuilder/translations/de/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/de/LC_MESSAGES/messages.po +305 -161
- flask_appbuilder/translations/fa/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/fa/LC_MESSAGES/messages.po +802 -0
- flask_appbuilder/translations/fr/LC_MESSAGES/messages.po +461 -319
- flask_appbuilder/translations/pt_BR/LC_MESSAGES/messages.po +650 -650
- flask_appbuilder/translations/ru/LC_MESSAGES/messages.po +1 -1
- flask_appbuilder/translations/sl/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/sl/LC_MESSAGES/messages.po +690 -0
- flask_appbuilder/translations/tr/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/tr/LC_MESSAGES/messages.po +1015 -0
- flask_appbuilder/upload.py +20 -22
- flask_appbuilder/urltools.py +39 -19
- flask_appbuilder/utils/base.py +76 -0
- flask_appbuilder/utils/legacy.py +33 -0
- flask_appbuilder/utils/limit.py +20 -0
- flask_appbuilder/validators.py +73 -14
- flask_appbuilder/views.py +75 -424
- flask_appbuilder/widgets.py +50 -51
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/METADATA +36 -76
- flask_appbuilder-5.0.2rc1.dist-info/RECORD +240 -0
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/WHEEL +1 -1
- flask_appbuilder-5.0.2rc1.dist-info/entry_points.txt +2 -0
- Flask_AppBuilder-3.2.1rc1.dist-info/RECORD +0 -270
- Flask_AppBuilder-3.2.1rc1.dist-info/entry_points.txt +0 -6
- flask_appbuilder/console.py +0 -426
- flask_appbuilder/models/mongoengine/__init__.py +0 -0
- flask_appbuilder/models/mongoengine/fields.py +0 -65
- flask_appbuilder/models/mongoengine/filters.py +0 -145
- flask_appbuilder/models/mongoengine/interface.py +0 -328
- flask_appbuilder/security/mongoengine/__init__.py +0 -0
- flask_appbuilder/security/mongoengine/manager.py +0 -402
- flask_appbuilder/security/mongoengine/models.py +0 -120
- flask_appbuilder/static/appbuilder/css/font-awesome.min.css +0 -4
- flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.css +0 -9
- flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.js +0 -28
- flask_appbuilder/static/appbuilder/fonts/FontAwesome.otf +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.eot +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.svg +0 -2671
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.ttf +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff2 +0 -0
- flask_appbuilder/static/appbuilder/img/aol.png +0 -0
- flask_appbuilder/static/appbuilder/img/flags/flags16.png +0 -0
- flask_appbuilder/static/appbuilder/img/flickr.png +0 -0
- flask_appbuilder/static/appbuilder/img/google.png +0 -0
- flask_appbuilder/static/appbuilder/img/myopenid.png +0 -0
- flask_appbuilder/static/appbuilder/img/yahoo.png +0 -0
- flask_appbuilder/static/appbuilder/js/_google_charts.js +0 -39
- flask_appbuilder/static/appbuilder/js/html5shiv.js +0 -8
- flask_appbuilder/static/appbuilder/js/respond.min.js +0 -6
- flask_appbuilder/static/appbuilder/select2/select2-spinner.gif +0 -0
- flask_appbuilder/static/appbuilder/select2/select2.css +0 -1205
- flask_appbuilder/static/appbuilder/select2/select2.js +0 -23
- flask_appbuilder/static/appbuilder/select2/select2.png +0 -0
- flask_appbuilder/static/appbuilder/select2/select2x2.png +0 -0
- flask_appbuilder/templates/appbuilder/general/security/login_oid.html +0 -129
- flask_appbuilder/templates/appbuilder/general/security/resetpassword.html +0 -29
- flask_appbuilder/tests/__init__.py +0 -0
- flask_appbuilder/tests/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_auth_ldap.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_auth_oauth.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_ldapsearch.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_oauth_registration_role.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/base.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/base.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/config_api.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/config_api.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/const.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/const.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_api.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_api.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_menu.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_menu.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mongoengine.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mvc.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mvc.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-37.pyc +0 -0
- flask_appbuilder/tests/_test_auth_ldap.py +0 -1045
- flask_appbuilder/tests/_test_auth_oauth.py +0 -419
- flask_appbuilder/tests/_test_ldapsearch.py +0 -135
- flask_appbuilder/tests/_test_oauth_registration_role.py +0 -59
- flask_appbuilder/tests/app.db +0 -0
- flask_appbuilder/tests/base.py +0 -90
- flask_appbuilder/tests/config_api.py +0 -21
- flask_appbuilder/tests/const.py +0 -9
- flask_appbuilder/tests/mongoengine/__init__.py +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/models.cpython-36.pyc +0 -0
- flask_appbuilder/tests/mongoengine/models.py +0 -41
- flask_appbuilder/tests/sqla/__init__.py +0 -0
- flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/models.cpython-36.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/models.cpython-37.pyc +0 -0
- flask_appbuilder/tests/sqla/models.py +0 -340
- flask_appbuilder/tests/test_0_fixture.py +0 -39
- flask_appbuilder/tests/test_api.py +0 -2790
- flask_appbuilder/tests/test_fab_cli.py +0 -72
- flask_appbuilder/tests/test_menu.py +0 -122
- flask_appbuilder/tests/test_mongoengine.py +0 -572
- flask_appbuilder/tests/test_mvc.py +0 -1710
- flask_appbuilder/tests/test_sqlalchemy.py +0 -24
- flask_appbuilder/translations/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/translations/es/LC_MESSAGES/messages.po~ +0 -582
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/LICENSE +0 -0
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/top_level.txt +0 -0
|
@@ -1,46 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
import logging
|
|
3
|
-
import sys
|
|
4
|
-
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
|
1
|
+
from __future__ import annotations
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Iterable, Optional, Tuple, Type
|
|
6
|
+
|
|
7
|
+
from flask import current_app, Request
|
|
8
|
+
from flask_appbuilder.exceptions import DatabaseException, FABException
|
|
9
|
+
from flask_appbuilder.filemanager import FileManager, ImageManager
|
|
10
|
+
from flask_appbuilder.models.base import BaseInterface
|
|
11
|
+
from flask_appbuilder.models.filters import Filters
|
|
12
|
+
from flask_appbuilder.models.group import GroupByCol, GroupByDateMonth, GroupByDateYear
|
|
13
|
+
from flask_appbuilder.models.mixins import FileColumn, ImageColumn
|
|
14
|
+
from flask_appbuilder.models.sqla import filters, Model
|
|
15
|
+
from flask_appbuilder.utils.base import (
|
|
16
|
+
get_column_leaf,
|
|
17
|
+
get_column_root_relation,
|
|
18
|
+
is_column_dotted,
|
|
19
|
+
)
|
|
7
20
|
from sqlalchemy import asc, desc
|
|
8
|
-
from sqlalchemy
|
|
9
|
-
from sqlalchemy.
|
|
21
|
+
from sqlalchemy import types as sa_types
|
|
22
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
23
|
+
from sqlalchemy.orm import aliased, class_mapper, ColumnProperty, contains_eager, Load
|
|
10
24
|
from sqlalchemy.orm.descriptor_props import SynonymProperty
|
|
25
|
+
from sqlalchemy.orm.properties import RelationshipProperty
|
|
11
26
|
from sqlalchemy.orm.query import Query
|
|
12
27
|
from sqlalchemy.orm.session import Session as SessionBase
|
|
13
28
|
from sqlalchemy.orm.util import AliasedClass
|
|
29
|
+
from sqlalchemy.sql import ColumnElement, visitors
|
|
14
30
|
from sqlalchemy.sql.elements import BinaryExpression
|
|
31
|
+
from sqlalchemy.sql.schema import Column
|
|
15
32
|
from sqlalchemy.sql.sqltypes import TypeEngine
|
|
16
33
|
from sqlalchemy_utils.types.uuid import UUIDType
|
|
17
34
|
|
|
18
|
-
|
|
19
|
-
from . import filters, Model
|
|
20
|
-
from ..base import BaseInterface
|
|
21
|
-
from ..filters import Filters
|
|
22
|
-
from ..group import GroupByCol, GroupByDateMonth, GroupByDateYear
|
|
23
|
-
from ..mixins import FileColumn, ImageColumn
|
|
24
|
-
from ..._compat import as_unicode
|
|
25
|
-
from ...const import (
|
|
26
|
-
LOGMSG_ERR_DBI_ADD_GENERIC,
|
|
27
|
-
LOGMSG_ERR_DBI_DEL_GENERIC,
|
|
28
|
-
LOGMSG_ERR_DBI_EDIT_GENERIC,
|
|
29
|
-
LOGMSG_WAR_DBI_ADD_INTEGRITY,
|
|
30
|
-
LOGMSG_WAR_DBI_DEL_INTEGRITY,
|
|
31
|
-
LOGMSG_WAR_DBI_EDIT_INTEGRITY,
|
|
32
|
-
)
|
|
33
|
-
from ...exceptions import InterfaceQueryWithoutSession
|
|
34
|
-
from ...filemanager import FileManager, ImageManager
|
|
35
|
-
from ...utils.base import get_column_leaf, get_column_root_relation, is_column_dotted
|
|
36
|
-
|
|
37
35
|
log = logging.getLogger(__name__)
|
|
38
36
|
|
|
39
37
|
|
|
40
38
|
def _is_sqla_type(model: Model, sa_type: Type[TypeEngine]) -> bool:
|
|
41
39
|
return (
|
|
42
40
|
isinstance(model, sa_type)
|
|
43
|
-
or isinstance(model,
|
|
41
|
+
or isinstance(model, sa_types.TypeDecorator)
|
|
44
42
|
and isinstance(model.impl, sa_type)
|
|
45
43
|
)
|
|
46
44
|
|
|
@@ -55,33 +53,76 @@ class SQLAInterface(BaseInterface):
|
|
|
55
53
|
|
|
56
54
|
def __init__(self, obj: Type[Model], session: Optional[SessionBase] = None) -> None:
|
|
57
55
|
_include_filters(self)
|
|
58
|
-
self.list_columns =
|
|
59
|
-
self.list_properties =
|
|
60
|
-
self.
|
|
56
|
+
self.list_columns = {}
|
|
57
|
+
self.list_properties = {}
|
|
58
|
+
self._session = session
|
|
61
59
|
# Collect all SQLA columns and properties
|
|
62
|
-
for prop in
|
|
60
|
+
for prop in class_mapper(obj).iterate_properties:
|
|
63
61
|
if type(prop) != SynonymProperty:
|
|
64
62
|
self.list_properties[prop.key] = prop
|
|
65
63
|
for col_name in obj.__mapper__.columns.keys():
|
|
66
64
|
if col_name in self.list_properties:
|
|
67
65
|
self.list_columns[col_name] = obj.__mapper__.columns[col_name]
|
|
68
|
-
super(
|
|
66
|
+
super().__init__(obj)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def session(self) -> SessionBase:
|
|
70
|
+
"""
|
|
71
|
+
Returns the SQLAlchemy session
|
|
72
|
+
"""
|
|
73
|
+
if not self._session:
|
|
74
|
+
return current_app.appbuilder.session
|
|
75
|
+
return self._session
|
|
69
76
|
|
|
70
77
|
@property
|
|
71
|
-
def model_name(self):
|
|
78
|
+
def model_name(self) -> str:
|
|
72
79
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
Returns the models class name
|
|
81
|
+
useful for auto title on views
|
|
75
82
|
"""
|
|
76
83
|
return self.obj.__name__
|
|
77
84
|
|
|
78
85
|
@staticmethod
|
|
79
86
|
def is_model_already_joined(query: Query, model: Type[Model]) -> bool:
|
|
80
|
-
|
|
87
|
+
if hasattr(query, "_join_entities"): # For SQLAlchemy < 1.3
|
|
88
|
+
return model in [mapper.class_ for mapper in query._join_entities]
|
|
89
|
+
# Solution for SQLAlchemy >= 1.4
|
|
90
|
+
model_table_name = model.__table__.fullname
|
|
91
|
+
for visitor in visitors.iterate(query.statement):
|
|
92
|
+
# Checking for `.join(Parent.child)` clauses
|
|
93
|
+
if visitor.__visit_name__ == "alias":
|
|
94
|
+
_visitor = visitor.element
|
|
95
|
+
else:
|
|
96
|
+
_visitor = visitor
|
|
97
|
+
if _visitor.__visit_name__ == "select":
|
|
98
|
+
continue
|
|
99
|
+
if _visitor.__visit_name__ == "binary":
|
|
100
|
+
for vis in visitors.iterate(_visitor):
|
|
101
|
+
# Visitor might not have table attribute
|
|
102
|
+
with suppress(AttributeError):
|
|
103
|
+
# Verify if already present based on table name
|
|
104
|
+
if model_table_name == vis.table.fullname:
|
|
105
|
+
return True
|
|
106
|
+
# Checking for `.join(Child)` clauses
|
|
107
|
+
if _visitor.__visit_name__ == "table":
|
|
108
|
+
# Visitor might be of ColumnCollection or so,
|
|
109
|
+
# which cannot be compared to model
|
|
110
|
+
if model_table_name == _visitor.fullname:
|
|
111
|
+
return True
|
|
112
|
+
# Checking for `Model.column` clauses
|
|
113
|
+
if _visitor.__visit_name__ == "column":
|
|
114
|
+
with suppress(AttributeError):
|
|
115
|
+
if model_table_name == _visitor.table.fullname:
|
|
116
|
+
return True
|
|
117
|
+
return False
|
|
81
118
|
|
|
82
119
|
def _get_base_query(
|
|
83
|
-
self,
|
|
84
|
-
|
|
120
|
+
self,
|
|
121
|
+
query: Query,
|
|
122
|
+
filters: Filters | None = None,
|
|
123
|
+
order_column: str = "",
|
|
124
|
+
order_direction: str = "",
|
|
125
|
+
) -> str:
|
|
85
126
|
if filters:
|
|
86
127
|
query = filters.apply_all(query)
|
|
87
128
|
return self.apply_order_by(query, order_column, order_direction)
|
|
@@ -90,7 +131,7 @@ class SQLAInterface(BaseInterface):
|
|
|
90
131
|
self,
|
|
91
132
|
query: Query,
|
|
92
133
|
root_relation: str,
|
|
93
|
-
aliases_mapping:
|
|
134
|
+
aliases_mapping: dict[str, AliasedClass] | None = None,
|
|
94
135
|
) -> Query:
|
|
95
136
|
"""
|
|
96
137
|
Helper function that applies necessary joins for dotted columns on a
|
|
@@ -134,7 +175,7 @@ class SQLAInterface(BaseInterface):
|
|
|
134
175
|
page
|
|
135
176
|
and page_size
|
|
136
177
|
and not order_column
|
|
137
|
-
and self.session.
|
|
178
|
+
and self.session.get_bind().name == "mssql"
|
|
138
179
|
):
|
|
139
180
|
pk_name = self.get_pk_name()
|
|
140
181
|
return query.order_by(pk_name)
|
|
@@ -145,7 +186,9 @@ class SQLAInterface(BaseInterface):
|
|
|
145
186
|
query: Query,
|
|
146
187
|
order_column: str,
|
|
147
188
|
order_direction: str,
|
|
148
|
-
aliases_mapping:
|
|
189
|
+
aliases_mapping: dict[str, AliasedClass] | None = None,
|
|
190
|
+
bypass_many_to_many: bool = False,
|
|
191
|
+
add_pk: bool = False,
|
|
149
192
|
) -> Query:
|
|
150
193
|
if order_column != "":
|
|
151
194
|
# if Model has custom decorator **renders('<COL_NAME>')**
|
|
@@ -157,6 +200,12 @@ class SQLAInterface(BaseInterface):
|
|
|
157
200
|
|
|
158
201
|
if is_column_dotted(order_column):
|
|
159
202
|
root_relation = get_column_root_relation(order_column)
|
|
203
|
+
if (
|
|
204
|
+
self.is_relation_many_to_many(root_relation)
|
|
205
|
+
or self.is_relation_one_to_many(root_relation)
|
|
206
|
+
and bypass_many_to_many
|
|
207
|
+
):
|
|
208
|
+
return query
|
|
160
209
|
# On MVC we still allow for joins to happen here
|
|
161
210
|
if not self.is_model_already_joined(
|
|
162
211
|
query, self.get_related_model(root_relation)
|
|
@@ -167,10 +216,15 @@ class SQLAInterface(BaseInterface):
|
|
|
167
216
|
column_leaf = get_column_leaf(order_column)
|
|
168
217
|
_alias = self.get_alias_mapping(root_relation, aliases_mapping)
|
|
169
218
|
_order_column = getattr(_alias, column_leaf)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
else
|
|
173
|
-
|
|
219
|
+
# get the primary key so we can add a tie breaker of the order
|
|
220
|
+
# when the order column is not unique it can cause issues with pagination
|
|
221
|
+
direction = asc if order_direction == "asc" else desc
|
|
222
|
+
order_by_columns = [direction(_order_column)]
|
|
223
|
+
pk = self.get_pk()
|
|
224
|
+
if add_pk and pk and pk != _order_column:
|
|
225
|
+
order_by_columns.append(direction(pk))
|
|
226
|
+
query = query.order_by(*order_by_columns)
|
|
227
|
+
|
|
174
228
|
return query
|
|
175
229
|
|
|
176
230
|
def apply_pagination(
|
|
@@ -189,23 +243,27 @@ class SQLAInterface(BaseInterface):
|
|
|
189
243
|
|
|
190
244
|
def _apply_normal_col_select_option(self, query: Query, column: str) -> Query:
|
|
191
245
|
if not self.is_relation(column) and not self.is_property_or_function(column):
|
|
192
|
-
return query.options(Load(self.obj).load_only(column))
|
|
246
|
+
return query.options(Load(self.obj).load_only(getattr(self.obj, column)))
|
|
193
247
|
return query
|
|
194
248
|
|
|
195
|
-
def _apply_relation_fks_select_options(
|
|
249
|
+
def _apply_relation_fks_select_options(
|
|
250
|
+
self, query: Query, relation_name: str
|
|
251
|
+
) -> Query:
|
|
196
252
|
relation = getattr(self.obj, relation_name)
|
|
197
253
|
if hasattr(relation, "property"):
|
|
198
254
|
local_cols = getattr(self.obj, relation_name).property.local_columns
|
|
199
255
|
for local_fk in local_cols:
|
|
200
|
-
query = query.options(
|
|
256
|
+
query = query.options(
|
|
257
|
+
Load(self.obj).load_only(getattr(self.obj, local_fk.name))
|
|
258
|
+
)
|
|
201
259
|
return query
|
|
202
260
|
return query
|
|
203
261
|
|
|
204
262
|
def apply_inner_select_joins(
|
|
205
263
|
self,
|
|
206
264
|
query: Query,
|
|
207
|
-
select_columns:
|
|
208
|
-
aliases_mapping:
|
|
265
|
+
select_columns: list[str] | None = None,
|
|
266
|
+
aliases_mapping: dict[str, AliasedClass] | None = None,
|
|
209
267
|
) -> Query:
|
|
210
268
|
"""
|
|
211
269
|
Add select load options to query. The goal
|
|
@@ -219,63 +277,115 @@ class SQLAInterface(BaseInterface):
|
|
|
219
277
|
"""
|
|
220
278
|
if not select_columns:
|
|
221
279
|
return query
|
|
222
|
-
|
|
280
|
+
|
|
281
|
+
joined_models = []
|
|
223
282
|
for column in select_columns:
|
|
224
|
-
if is_column_dotted(column):
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
)
|
|
241
|
-
joined_models.append(root_relation)
|
|
242
|
-
|
|
243
|
-
related_model_ = self.get_alias_mapping(
|
|
244
|
-
root_relation, aliases_mapping
|
|
283
|
+
if not is_column_dotted(column):
|
|
284
|
+
query = self._apply_normal_col_select_option(query, column)
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
# Dotted column
|
|
288
|
+
root_relation = get_column_root_relation(column)
|
|
289
|
+
leaf_column = get_column_leaf(column)
|
|
290
|
+
related_model = self.get_alias_mapping(root_relation, aliases_mapping)
|
|
291
|
+
relation = getattr(self.obj, root_relation)
|
|
292
|
+
|
|
293
|
+
if self.is_relation_many_to_one(
|
|
294
|
+
root_relation
|
|
295
|
+
) or self.is_relation_many_to_many_special(root_relation):
|
|
296
|
+
if root_relation not in joined_models:
|
|
297
|
+
query = self._query_join_relation(
|
|
298
|
+
query, root_relation, aliases_mapping=aliases_mapping
|
|
245
299
|
)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
# https://docs.sqlalchemy.org/en/13/orm/loading_relationships.html
|
|
249
|
-
query = query.options(
|
|
250
|
-
contains_eager(relation.of_type(related_model_)).load_only(
|
|
251
|
-
leaf_column
|
|
252
|
-
)
|
|
300
|
+
query = query.add_entity(
|
|
301
|
+
self.get_alias_mapping(root_relation, aliases_mapping)
|
|
253
302
|
)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
303
|
+
# Add relation FK to avoid N+1 performance issue
|
|
304
|
+
query = self._apply_relation_fks_select_options(
|
|
305
|
+
query, root_relation
|
|
306
|
+
)
|
|
307
|
+
joined_models.append(root_relation)
|
|
308
|
+
|
|
309
|
+
related_model = self.get_alias_mapping(root_relation, aliases_mapping)
|
|
310
|
+
relation = getattr(self.obj, root_relation)
|
|
311
|
+
# The Zen of eager loading :(
|
|
312
|
+
# https://docs.sqlalchemy.org/en/13/orm/loading_relationships.html
|
|
313
|
+
query = query.options(
|
|
314
|
+
contains_eager(relation.of_type(related_model)).load_only(
|
|
315
|
+
getattr(related_model, leaf_column)
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
query = query.options(
|
|
319
|
+
Load(related_model).load_only(getattr(related_model, leaf_column))
|
|
320
|
+
)
|
|
257
321
|
return query
|
|
258
322
|
|
|
323
|
+
def get_outer_query_from_inner_query(
|
|
324
|
+
self, query: Query, inner_query: Query
|
|
325
|
+
) -> Query:
|
|
326
|
+
pk = self.get_pk()
|
|
327
|
+
pk_name = self.get_pk_name()
|
|
328
|
+
|
|
329
|
+
if isinstance(pk_name, str):
|
|
330
|
+
# Only select the primary key in the inner query
|
|
331
|
+
inner_query = inner_query.with_entities(pk)
|
|
332
|
+
|
|
333
|
+
subquery = inner_query.subquery()
|
|
334
|
+
subquery_pk: ColumnElement = getattr(subquery.c, pk_name)
|
|
335
|
+
return query.join(subquery, pk == subquery_pk)
|
|
336
|
+
|
|
337
|
+
if isinstance(pk_name, Iterable):
|
|
338
|
+
raise FABException("Composite primary key not supported")
|
|
339
|
+
|
|
340
|
+
raise FABException("No primary key found")
|
|
341
|
+
|
|
259
342
|
def apply_outer_select_joins(
|
|
260
|
-
self,
|
|
343
|
+
self,
|
|
344
|
+
query: Query,
|
|
345
|
+
select_columns: list[str] | None = None,
|
|
346
|
+
outer_default_load: bool = False,
|
|
347
|
+
aliases_mapping: dict[str, AliasedClass] | None = None,
|
|
261
348
|
) -> Query:
|
|
262
349
|
if not select_columns:
|
|
263
350
|
return query
|
|
351
|
+
|
|
352
|
+
aliases_mapping = aliases_mapping or {}
|
|
353
|
+
|
|
264
354
|
for column in select_columns:
|
|
265
|
-
if is_column_dotted(column):
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
355
|
+
if not is_column_dotted(column):
|
|
356
|
+
query = self._apply_normal_col_select_option(query, column)
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
root_relation = get_column_root_relation(column)
|
|
360
|
+
related_model = self.get_related_model(root_relation)
|
|
361
|
+
|
|
362
|
+
# Use meaningful alias if not already defined
|
|
363
|
+
if root_relation not in aliases_mapping:
|
|
364
|
+
alias = aliased(related_model, name=root_relation)
|
|
365
|
+
aliases_mapping[root_relation] = alias
|
|
366
|
+
else:
|
|
367
|
+
alias = aliases_mapping[root_relation]
|
|
368
|
+
|
|
369
|
+
attr = getattr(self.obj, root_relation)
|
|
370
|
+
leaf_column = getattr(alias, get_column_leaf(column))
|
|
371
|
+
if self.is_relation_many_to_many(
|
|
372
|
+
root_relation
|
|
373
|
+
) or self.is_relation_one_to_many(root_relation):
|
|
374
|
+
if outer_default_load:
|
|
375
|
+
load = (
|
|
376
|
+
Load(self.obj)
|
|
377
|
+
.defaultload(attr)
|
|
378
|
+
.load_only(getattr(related_model, get_column_leaf(column)))
|
|
273
379
|
)
|
|
274
380
|
else:
|
|
275
|
-
|
|
276
|
-
|
|
381
|
+
query = query.join(alias, attr, isouter=True)
|
|
382
|
+
load = contains_eager(attr.of_type(alias)).load_only(leaf_column)
|
|
277
383
|
else:
|
|
278
|
-
query =
|
|
384
|
+
query = query.join(alias, attr, isouter=True)
|
|
385
|
+
load = contains_eager(attr.of_type(alias)).load_only(leaf_column)
|
|
386
|
+
|
|
387
|
+
query = query.options(load)
|
|
388
|
+
|
|
279
389
|
return query
|
|
280
390
|
|
|
281
391
|
def get_inner_filters(self, filters: Optional[Filters]) -> Filters:
|
|
@@ -293,25 +403,28 @@ class SQLAInterface(BaseInterface):
|
|
|
293
403
|
if not is_column_dotted(flt.column_name):
|
|
294
404
|
_filters.append((flt.column_name, flt.__class__, value))
|
|
295
405
|
elif self.is_relation_many_to_one(
|
|
296
|
-
flt.column_name
|
|
297
|
-
) or self.is_relation_one_to_one(
|
|
406
|
+
get_column_root_relation(flt.column_name)
|
|
407
|
+
) or self.is_relation_one_to_one(
|
|
408
|
+
get_column_root_relation(flt.column_name)
|
|
409
|
+
):
|
|
298
410
|
_filters.append((flt.column_name, flt.__class__, value))
|
|
299
411
|
inner_filters.add_filter_list(_filters)
|
|
300
412
|
return inner_filters
|
|
301
413
|
|
|
302
|
-
def exists_col_to_many(self, select_columns:
|
|
414
|
+
def exists_col_to_many(self, select_columns: list[str]) -> bool:
|
|
303
415
|
for column in select_columns:
|
|
304
|
-
if is_column_dotted(column):
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
416
|
+
if not is_column_dotted(column):
|
|
417
|
+
continue
|
|
418
|
+
root_relation = get_column_root_relation(column)
|
|
419
|
+
if self.is_relation_many_to_many(
|
|
420
|
+
root_relation
|
|
421
|
+
) or self.is_relation_one_to_many(root_relation):
|
|
422
|
+
return True
|
|
310
423
|
return False
|
|
311
424
|
|
|
312
425
|
def get_alias_mapping(
|
|
313
|
-
self, model_name: str, aliases_mapping:
|
|
314
|
-
) ->
|
|
426
|
+
self, model_name: str, aliases_mapping: dict[str, AliasedClass] | None
|
|
427
|
+
) -> AliasedClass | Type[Model]:
|
|
315
428
|
if aliases_mapping is None:
|
|
316
429
|
return self.get_related_model(model_name)
|
|
317
430
|
return aliases_mapping.get(model_name, self.get_related_model(model_name))
|
|
@@ -319,20 +432,25 @@ class SQLAInterface(BaseInterface):
|
|
|
319
432
|
def _apply_inner_all(
|
|
320
433
|
self,
|
|
321
434
|
query: Query,
|
|
322
|
-
filters:
|
|
435
|
+
filters: Filters | None = None,
|
|
323
436
|
order_column: str = "",
|
|
324
437
|
order_direction: str = "",
|
|
325
|
-
page:
|
|
326
|
-
page_size:
|
|
327
|
-
select_columns:
|
|
328
|
-
aliases_mapping:
|
|
438
|
+
page: int | None = None,
|
|
439
|
+
page_size: int | None = None,
|
|
440
|
+
select_columns: list[str] | None = None,
|
|
441
|
+
aliases_mapping: dict[str, AliasedClass] | None = None,
|
|
329
442
|
) -> Query:
|
|
330
443
|
inner_filters = self.get_inner_filters(filters)
|
|
331
444
|
query = self.apply_inner_select_joins(query, select_columns, aliases_mapping)
|
|
332
445
|
query = self.apply_filters(query, inner_filters)
|
|
333
446
|
query = self.apply_engine_specific_hack(query, page, page_size, order_column)
|
|
334
447
|
query = self.apply_order_by(
|
|
335
|
-
query,
|
|
448
|
+
query,
|
|
449
|
+
order_column,
|
|
450
|
+
order_direction,
|
|
451
|
+
aliases_mapping=aliases_mapping,
|
|
452
|
+
bypass_many_to_many=True,
|
|
453
|
+
add_pk=True,
|
|
336
454
|
)
|
|
337
455
|
query = self.apply_pagination(query, page, page_size)
|
|
338
456
|
return query
|
|
@@ -341,7 +459,7 @@ class SQLAInterface(BaseInterface):
|
|
|
341
459
|
self,
|
|
342
460
|
query: Query,
|
|
343
461
|
filters: Optional[Filters] = None,
|
|
344
|
-
select_columns: Optional[
|
|
462
|
+
select_columns: Optional[list[str]] = None,
|
|
345
463
|
) -> int:
|
|
346
464
|
return self._apply_inner_all(
|
|
347
465
|
query, filters, select_columns=select_columns, aliases_mapping={}
|
|
@@ -355,7 +473,8 @@ class SQLAInterface(BaseInterface):
|
|
|
355
473
|
order_direction: str = "",
|
|
356
474
|
page: Optional[int] = None,
|
|
357
475
|
page_size: Optional[int] = None,
|
|
358
|
-
select_columns: Optional[
|
|
476
|
+
select_columns: Optional[list[str]] = None,
|
|
477
|
+
outer_default_load: bool = False,
|
|
359
478
|
) -> Query:
|
|
360
479
|
"""
|
|
361
480
|
Accepts a SQLAlchemy Query and applies all filtering logic, order by and
|
|
@@ -374,9 +493,14 @@ class SQLAInterface(BaseInterface):
|
|
|
374
493
|
the current page size
|
|
375
494
|
:param select_columns:
|
|
376
495
|
A List of columns to be specifically selected on the query
|
|
496
|
+
:param outer_default_load: If True, the default load for outer joins will be
|
|
497
|
+
applied. This is useful for when you want to control
|
|
498
|
+
the load of the many-to-many relationships at the model level.
|
|
499
|
+
we will apply:
|
|
500
|
+
https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#sqlalchemy.orm.Load.defaultload
|
|
377
501
|
:return: A SQLAlchemy Query with all the applied logic
|
|
378
502
|
"""
|
|
379
|
-
aliases_mapping = {}
|
|
503
|
+
aliases_mapping: dict[str, AliasedClass] = {}
|
|
380
504
|
inner_query = self._apply_inner_all(
|
|
381
505
|
query,
|
|
382
506
|
filters,
|
|
@@ -391,9 +515,19 @@ class SQLAInterface(BaseInterface):
|
|
|
391
515
|
if select_columns and self.exists_col_to_many(select_columns):
|
|
392
516
|
if select_columns and order_column:
|
|
393
517
|
select_columns = select_columns + [order_column]
|
|
394
|
-
outer_query =
|
|
395
|
-
outer_query = self.apply_outer_select_joins(
|
|
396
|
-
|
|
518
|
+
outer_query = self.get_outer_query_from_inner_query(query, inner_query)
|
|
519
|
+
outer_query = self.apply_outer_select_joins(
|
|
520
|
+
outer_query,
|
|
521
|
+
select_columns,
|
|
522
|
+
outer_default_load=outer_default_load,
|
|
523
|
+
aliases_mapping=aliases_mapping,
|
|
524
|
+
)
|
|
525
|
+
return self.apply_order_by(
|
|
526
|
+
outer_query,
|
|
527
|
+
order_column,
|
|
528
|
+
order_direction,
|
|
529
|
+
aliases_mapping=aliases_mapping,
|
|
530
|
+
)
|
|
397
531
|
else:
|
|
398
532
|
return inner_query
|
|
399
533
|
|
|
@@ -404,8 +538,9 @@ class SQLAInterface(BaseInterface):
|
|
|
404
538
|
order_direction: str = "",
|
|
405
539
|
page: Optional[int] = None,
|
|
406
540
|
page_size: Optional[int] = None,
|
|
407
|
-
select_columns: Optional[
|
|
408
|
-
|
|
541
|
+
select_columns: Optional[list[str]] = None,
|
|
542
|
+
outer_default_load: bool = False,
|
|
543
|
+
) -> Tuple[int, list[Model]]:
|
|
409
544
|
"""
|
|
410
545
|
Returns the results for a model query, applies filters, sorting and pagination
|
|
411
546
|
|
|
@@ -416,10 +551,13 @@ class SQLAInterface(BaseInterface):
|
|
|
416
551
|
:param page_size: the current page size
|
|
417
552
|
:param select_columns: A List of columns to be specifically selected
|
|
418
553
|
on the query. Supports dotted notation.
|
|
554
|
+
:param outer_default_load: If True, the default load for outer joins will be
|
|
555
|
+
applied. This is useful for when you want to control
|
|
556
|
+
the load of the many-to-many relationships at the model level.
|
|
557
|
+
we will apply:
|
|
558
|
+
https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#sqlalchemy.orm.Load.defaultload
|
|
419
559
|
:return: A tuple with the query count (non paginated) and the results
|
|
420
560
|
"""
|
|
421
|
-
if not self.session:
|
|
422
|
-
raise InterfaceQueryWithoutSession()
|
|
423
561
|
query = self.session.query(self.obj)
|
|
424
562
|
|
|
425
563
|
count = self.query_count(query, filters, select_columns)
|
|
@@ -434,7 +572,7 @@ class SQLAInterface(BaseInterface):
|
|
|
434
572
|
)
|
|
435
573
|
query_results = query.all()
|
|
436
574
|
|
|
437
|
-
result =
|
|
575
|
+
result = []
|
|
438
576
|
for item in query_results:
|
|
439
577
|
if hasattr(item, self.obj.__name__):
|
|
440
578
|
result.append(getattr(item, self.obj.__name__))
|
|
@@ -443,22 +581,26 @@ class SQLAInterface(BaseInterface):
|
|
|
443
581
|
return count, result
|
|
444
582
|
|
|
445
583
|
def query_simple_group(
|
|
446
|
-
self, group_by
|
|
447
|
-
):
|
|
584
|
+
self, group_by: str | None = None, filters: Filters | None = None
|
|
585
|
+
) -> list[list[Any]]:
|
|
448
586
|
query = self.session.query(self.obj)
|
|
449
587
|
query = self._get_base_query(query=query, filters=filters)
|
|
450
588
|
query_result = query.all()
|
|
451
589
|
group = GroupByCol(group_by, "Group by")
|
|
452
590
|
return group.apply(query_result)
|
|
453
591
|
|
|
454
|
-
def query_month_group(
|
|
592
|
+
def query_month_group(
|
|
593
|
+
self, group_by: str | None = None, filters: Filters | None = None
|
|
594
|
+
) -> list[list[Any]]:
|
|
455
595
|
query = self.session.query(self.obj)
|
|
456
596
|
query = self._get_base_query(query=query, filters=filters)
|
|
457
597
|
query_result = query.all()
|
|
458
598
|
group = GroupByDateMonth(group_by, "Group by Month")
|
|
459
599
|
return group.apply(query_result)
|
|
460
600
|
|
|
461
|
-
def query_year_group(
|
|
601
|
+
def query_year_group(
|
|
602
|
+
self, group_by: str | None = None, filters: Filters | None = None
|
|
603
|
+
) -> list[list[Any]]:
|
|
462
604
|
query = self.session.query(self.obj)
|
|
463
605
|
query = self._get_base_query(query=query, filters=filters)
|
|
464
606
|
query_result = query.all()
|
|
@@ -486,7 +628,7 @@ class SQLAInterface(BaseInterface):
|
|
|
486
628
|
def is_string(self, col_name: str) -> bool:
|
|
487
629
|
try:
|
|
488
630
|
return (
|
|
489
|
-
_is_sqla_type(self.list_columns[col_name].type,
|
|
631
|
+
_is_sqla_type(self.list_columns[col_name].type, sa_types.String)
|
|
490
632
|
or self.list_columns[col_name].type.__class__ == UUIDType
|
|
491
633
|
)
|
|
492
634
|
except KeyError:
|
|
@@ -494,63 +636,67 @@ class SQLAInterface(BaseInterface):
|
|
|
494
636
|
|
|
495
637
|
def is_text(self, col_name: str) -> bool:
|
|
496
638
|
try:
|
|
497
|
-
return _is_sqla_type(self.list_columns[col_name].type,
|
|
639
|
+
return _is_sqla_type(self.list_columns[col_name].type, sa_types.Text)
|
|
498
640
|
except KeyError:
|
|
499
641
|
return False
|
|
500
642
|
|
|
501
643
|
def is_binary(self, col_name: str) -> bool:
|
|
502
644
|
try:
|
|
503
|
-
return _is_sqla_type(self.list_columns[col_name].type,
|
|
645
|
+
return _is_sqla_type(self.list_columns[col_name].type, sa_types.LargeBinary)
|
|
504
646
|
except KeyError:
|
|
505
647
|
return False
|
|
506
648
|
|
|
507
649
|
def is_integer(self, col_name: str) -> bool:
|
|
508
650
|
try:
|
|
509
|
-
return _is_sqla_type(self.list_columns[col_name].type,
|
|
651
|
+
return _is_sqla_type(self.list_columns[col_name].type, sa_types.Integer)
|
|
510
652
|
except KeyError:
|
|
511
653
|
return False
|
|
512
654
|
|
|
513
655
|
def is_numeric(self, col_name: str) -> bool:
|
|
514
656
|
try:
|
|
515
|
-
return _is_sqla_type(self.list_columns[col_name].type,
|
|
657
|
+
return _is_sqla_type(self.list_columns[col_name].type, sa_types.Numeric)
|
|
516
658
|
except KeyError:
|
|
517
659
|
return False
|
|
518
660
|
|
|
519
661
|
def is_float(self, col_name: str) -> bool:
|
|
520
662
|
try:
|
|
521
|
-
return _is_sqla_type(self.list_columns[col_name].type,
|
|
663
|
+
return _is_sqla_type(self.list_columns[col_name].type, sa_types.Float)
|
|
522
664
|
except KeyError:
|
|
523
665
|
return False
|
|
524
666
|
|
|
525
667
|
def is_boolean(self, col_name: str) -> bool:
|
|
526
668
|
try:
|
|
527
|
-
return _is_sqla_type(self.list_columns[col_name].type,
|
|
669
|
+
return _is_sqla_type(self.list_columns[col_name].type, sa_types.Boolean)
|
|
528
670
|
except KeyError:
|
|
529
671
|
return False
|
|
530
672
|
|
|
531
673
|
def is_date(self, col_name: str) -> bool:
|
|
532
674
|
try:
|
|
533
|
-
return _is_sqla_type(self.list_columns[col_name].type,
|
|
675
|
+
return _is_sqla_type(self.list_columns[col_name].type, sa_types.Date)
|
|
534
676
|
except KeyError:
|
|
535
677
|
return False
|
|
536
678
|
|
|
537
679
|
def is_datetime(self, col_name: str) -> bool:
|
|
538
680
|
try:
|
|
539
|
-
return _is_sqla_type(self.list_columns[col_name].type,
|
|
681
|
+
return _is_sqla_type(self.list_columns[col_name].type, sa_types.DateTime)
|
|
540
682
|
except KeyError:
|
|
541
683
|
return False
|
|
542
684
|
|
|
543
685
|
def is_enum(self, col_name: str) -> bool:
|
|
544
686
|
try:
|
|
545
|
-
return _is_sqla_type(self.list_columns[col_name].type,
|
|
687
|
+
return _is_sqla_type(self.list_columns[col_name].type, sa_types.Enum)
|
|
688
|
+
except KeyError:
|
|
689
|
+
return False
|
|
690
|
+
|
|
691
|
+
def is_json(self, col_name: str) -> bool:
|
|
692
|
+
try:
|
|
693
|
+
return _is_sqla_type(self.list_columns[col_name].type, sa_types.JSON)
|
|
546
694
|
except KeyError:
|
|
547
695
|
return False
|
|
548
696
|
|
|
549
697
|
def is_relation(self, col_name: str) -> bool:
|
|
550
698
|
try:
|
|
551
|
-
return isinstance(
|
|
552
|
-
self.list_properties[col_name], sa.orm.properties.RelationshipProperty
|
|
553
|
-
)
|
|
699
|
+
return isinstance(self.list_properties[col_name], RelationshipProperty)
|
|
554
700
|
except KeyError:
|
|
555
701
|
return False
|
|
556
702
|
|
|
@@ -565,7 +711,17 @@ class SQLAInterface(BaseInterface):
|
|
|
565
711
|
def is_relation_many_to_many(self, col_name: str) -> bool:
|
|
566
712
|
try:
|
|
567
713
|
if self.is_relation(col_name):
|
|
568
|
-
|
|
714
|
+
relation = self.list_properties[col_name]
|
|
715
|
+
return relation.direction.name == "MANYTOMANY"
|
|
716
|
+
return False
|
|
717
|
+
except KeyError:
|
|
718
|
+
return False
|
|
719
|
+
|
|
720
|
+
def is_relation_many_to_many_special(self, col_name: str) -> bool:
|
|
721
|
+
try:
|
|
722
|
+
if self.is_relation(col_name):
|
|
723
|
+
relation = self.list_properties[col_name]
|
|
724
|
+
return relation.direction.name == "ONETOONE" and relation.uselist
|
|
569
725
|
return False
|
|
570
726
|
except KeyError:
|
|
571
727
|
return False
|
|
@@ -573,7 +729,10 @@ class SQLAInterface(BaseInterface):
|
|
|
573
729
|
def is_relation_one_to_one(self, col_name: str) -> bool:
|
|
574
730
|
try:
|
|
575
731
|
if self.is_relation(col_name):
|
|
576
|
-
|
|
732
|
+
relation = self.list_properties[col_name]
|
|
733
|
+
return self.list_properties[col_name].direction.name == "ONETOONE" or (
|
|
734
|
+
relation.direction.name == "ONETOMANY" and relation.uselist is False
|
|
735
|
+
)
|
|
577
736
|
return False
|
|
578
737
|
except KeyError:
|
|
579
738
|
return False
|
|
@@ -581,7 +740,8 @@ class SQLAInterface(BaseInterface):
|
|
|
581
740
|
def is_relation_one_to_many(self, col_name: str) -> bool:
|
|
582
741
|
try:
|
|
583
742
|
if self.is_relation(col_name):
|
|
584
|
-
|
|
743
|
+
relation = self.list_properties[col_name]
|
|
744
|
+
return relation.direction.name == "ONETOMANY" and relation.uselist
|
|
585
745
|
return False
|
|
586
746
|
except KeyError:
|
|
587
747
|
return False
|
|
@@ -643,100 +803,47 @@ class SQLAInterface(BaseInterface):
|
|
|
643
803
|
-------------------------------
|
|
644
804
|
"""
|
|
645
805
|
|
|
646
|
-
def add(self, item: Model,
|
|
806
|
+
def add(self, item: Model, commit: bool = True) -> None:
|
|
647
807
|
try:
|
|
648
808
|
self.session.add(item)
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
self.message = (as_unicode(self.add_integrity_error_message), "warning")
|
|
654
|
-
log.warning(LOGMSG_WAR_DBI_ADD_INTEGRITY.format(str(e)))
|
|
655
|
-
self.session.rollback()
|
|
656
|
-
if raise_exception:
|
|
657
|
-
raise e
|
|
658
|
-
return False
|
|
659
|
-
except Exception as e:
|
|
660
|
-
self.message = (
|
|
661
|
-
as_unicode(self.general_error_message + " " + str(sys.exc_info()[0])),
|
|
662
|
-
"danger",
|
|
663
|
-
)
|
|
664
|
-
log.exception(LOGMSG_ERR_DBI_ADD_GENERIC.format(str(e)))
|
|
809
|
+
if commit:
|
|
810
|
+
self.session.commit()
|
|
811
|
+
except SQLAlchemyError as ex:
|
|
812
|
+
log.exception("Add item database error")
|
|
665
813
|
self.session.rollback()
|
|
666
|
-
|
|
667
|
-
raise e
|
|
668
|
-
return False
|
|
814
|
+
raise ex
|
|
669
815
|
|
|
670
|
-
def edit(self, item: Model,
|
|
816
|
+
def edit(self, item: Model, commit: bool = True) -> None:
|
|
671
817
|
try:
|
|
672
818
|
self.session.merge(item)
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
self.message = (as_unicode(self.edit_integrity_error_message), "warning")
|
|
678
|
-
log.warning(LOGMSG_WAR_DBI_EDIT_INTEGRITY.format(str(e)))
|
|
679
|
-
self.session.rollback()
|
|
680
|
-
if raise_exception:
|
|
681
|
-
raise e
|
|
682
|
-
return False
|
|
683
|
-
except Exception as e:
|
|
684
|
-
self.message = (
|
|
685
|
-
as_unicode(self.general_error_message + " " + str(sys.exc_info()[0])),
|
|
686
|
-
"danger",
|
|
687
|
-
)
|
|
688
|
-
log.exception(LOGMSG_ERR_DBI_EDIT_GENERIC.format(str(e)))
|
|
819
|
+
if commit:
|
|
820
|
+
self.session.commit()
|
|
821
|
+
except SQLAlchemyError as ex:
|
|
822
|
+
log.exception("Edit item database error")
|
|
689
823
|
self.session.rollback()
|
|
690
|
-
|
|
691
|
-
raise e
|
|
692
|
-
return False
|
|
824
|
+
raise DatabaseException from ex
|
|
693
825
|
|
|
694
|
-
def delete(self, item: Model,
|
|
826
|
+
def delete(self, item: Model, commit: bool = True) -> None:
|
|
695
827
|
try:
|
|
696
828
|
self._delete_files(item)
|
|
697
829
|
self.session.delete(item)
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
self.message = (as_unicode(self.delete_integrity_error_message), "warning")
|
|
703
|
-
log.warning(LOGMSG_WAR_DBI_DEL_INTEGRITY.format(str(e)))
|
|
704
|
-
self.session.rollback()
|
|
705
|
-
if raise_exception:
|
|
706
|
-
raise e
|
|
707
|
-
return False
|
|
708
|
-
except Exception as e:
|
|
709
|
-
self.message = (
|
|
710
|
-
as_unicode(self.general_error_message + " " + str(sys.exc_info()[0])),
|
|
711
|
-
"danger",
|
|
712
|
-
)
|
|
713
|
-
log.exception(LOGMSG_ERR_DBI_DEL_GENERIC.format(str(e)))
|
|
830
|
+
if commit:
|
|
831
|
+
self.session.commit()
|
|
832
|
+
except SQLAlchemyError as ex:
|
|
833
|
+
log.exception("Delete item database error")
|
|
714
834
|
self.session.rollback()
|
|
715
|
-
|
|
716
|
-
raise e
|
|
717
|
-
return False
|
|
835
|
+
raise DatabaseException from ex
|
|
718
836
|
|
|
719
|
-
def delete_all(self, items:
|
|
837
|
+
def delete_all(self, items: list[Model]) -> None:
|
|
720
838
|
try:
|
|
721
839
|
for item in items:
|
|
722
840
|
self._delete_files(item)
|
|
723
841
|
self.session.delete(item)
|
|
724
842
|
self.session.commit()
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
except IntegrityError as e:
|
|
728
|
-
self.message = (as_unicode(self.delete_integrity_error_message), "warning")
|
|
729
|
-
log.warning(LOGMSG_WAR_DBI_DEL_INTEGRITY.format(str(e)))
|
|
730
|
-
self.session.rollback()
|
|
731
|
-
return False
|
|
732
|
-
except Exception as e:
|
|
733
|
-
self.message = (
|
|
734
|
-
as_unicode(self.general_error_message + " " + str(sys.exc_info()[0])),
|
|
735
|
-
"danger",
|
|
736
|
-
)
|
|
737
|
-
log.exception(LOGMSG_ERR_DBI_DEL_GENERIC.format(str(e)))
|
|
843
|
+
except SQLAlchemyError as ex:
|
|
844
|
+
log.exception("Delete items database error")
|
|
738
845
|
self.session.rollback()
|
|
739
|
-
|
|
846
|
+
raise DatabaseException from ex
|
|
740
847
|
|
|
741
848
|
"""
|
|
742
849
|
-----------------------
|
|
@@ -744,27 +851,24 @@ class SQLAInterface(BaseInterface):
|
|
|
744
851
|
-----------------------
|
|
745
852
|
"""
|
|
746
853
|
|
|
747
|
-
def _add_files(self, this_request, item: Model):
|
|
854
|
+
def _add_files(self, this_request: Request, item: Model) -> None:
|
|
748
855
|
fm = FileManager()
|
|
749
856
|
im = ImageManager()
|
|
750
857
|
for file_col in this_request.files:
|
|
751
858
|
if self.is_file(file_col):
|
|
752
859
|
fm.save_file(this_request.files[file_col], getattr(item, file_col))
|
|
753
|
-
|
|
754
|
-
if self.is_image(file_col):
|
|
860
|
+
elif self.is_image(file_col):
|
|
755
861
|
im.save_file(this_request.files[file_col], getattr(item, file_col))
|
|
756
862
|
|
|
757
|
-
def _delete_files(self, item: Model):
|
|
863
|
+
def _delete_files(self, item: Model) -> None:
|
|
758
864
|
for file_col in self.get_file_column_list():
|
|
759
|
-
if self.is_file(file_col):
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
fm.delete_file(getattr(item, file_col))
|
|
865
|
+
if self.is_file(file_col) and getattr(item, file_col):
|
|
866
|
+
fm = FileManager()
|
|
867
|
+
fm.delete_file(getattr(item, file_col))
|
|
763
868
|
for file_col in self.get_image_column_list():
|
|
764
|
-
if self.is_image(file_col):
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
im.delete_file(getattr(item, file_col))
|
|
869
|
+
if self.is_image(file_col) and getattr(item, file_col):
|
|
870
|
+
im = ImageManager()
|
|
871
|
+
im.delete_file(getattr(item, file_col))
|
|
768
872
|
|
|
769
873
|
"""
|
|
770
874
|
------------------------------
|
|
@@ -774,22 +878,27 @@ class SQLAInterface(BaseInterface):
|
|
|
774
878
|
|
|
775
879
|
def get_col_default(self, col_name: str) -> Any:
|
|
776
880
|
default = getattr(self.list_columns[col_name], "default", None)
|
|
777
|
-
if default is
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
881
|
+
if default is None:
|
|
882
|
+
return None
|
|
883
|
+
|
|
884
|
+
value = getattr(default, "arg", None)
|
|
885
|
+
if value is None:
|
|
886
|
+
return None
|
|
887
|
+
|
|
888
|
+
if getattr(default, "is_callable", False):
|
|
889
|
+
return lambda: default.arg(None)
|
|
890
|
+
|
|
891
|
+
if not getattr(default, "is_scalar", True):
|
|
892
|
+
return None
|
|
893
|
+
|
|
894
|
+
return value
|
|
786
895
|
|
|
787
896
|
def get_related_model(self, col_name: str) -> Type[Model]:
|
|
788
897
|
return self.list_properties[col_name].mapper.class_
|
|
789
898
|
|
|
790
899
|
def get_related_model_and_join(
|
|
791
900
|
self, col_name: str
|
|
792
|
-
) ->
|
|
901
|
+
) -> list[Tuple[Type[Model], BinaryExpression]]:
|
|
793
902
|
relation = self.list_properties[col_name]
|
|
794
903
|
if relation.direction.name == "MANYTOMANY":
|
|
795
904
|
return [
|
|
@@ -798,16 +907,14 @@ class SQLAInterface(BaseInterface):
|
|
|
798
907
|
]
|
|
799
908
|
return [(relation.mapper.class_, relation.primaryjoin)]
|
|
800
909
|
|
|
801
|
-
def get_related_interface(self, col_name: str):
|
|
802
|
-
return self.__class__(self.get_related_model(col_name)
|
|
910
|
+
def get_related_interface(self, col_name: str) -> BaseInterface:
|
|
911
|
+
return self.__class__(self.get_related_model(col_name))
|
|
803
912
|
|
|
804
913
|
def get_related_obj(self, col_name: str, value: Any) -> Optional[Type[Model]]:
|
|
805
914
|
rel_model = self.get_related_model(col_name)
|
|
806
|
-
|
|
807
|
-
return self.session.query(rel_model).get(value)
|
|
808
|
-
return None
|
|
915
|
+
return self.session.query(rel_model).get(value)
|
|
809
916
|
|
|
810
|
-
def get_related_fks(self, related_views) ->
|
|
917
|
+
def get_related_fks(self, related_views: Any) -> list[str]:
|
|
811
918
|
return [view.datamodel.get_related_fk(self.obj) for view in related_views]
|
|
812
919
|
|
|
813
920
|
def get_related_fk(self, model: Type[Model]) -> Optional[str]:
|
|
@@ -817,7 +924,7 @@ class SQLAInterface(BaseInterface):
|
|
|
817
924
|
return col_name
|
|
818
925
|
return None
|
|
819
926
|
|
|
820
|
-
def get_info(self, col_name: str):
|
|
927
|
+
def get_info(self, col_name: str) -> dict[str, Any]:
|
|
821
928
|
if col_name in self.list_properties:
|
|
822
929
|
return self.list_properties[col_name].info
|
|
823
930
|
return {}
|
|
@@ -828,25 +935,25 @@ class SQLAInterface(BaseInterface):
|
|
|
828
935
|
-------------
|
|
829
936
|
"""
|
|
830
937
|
|
|
831
|
-
def get_columns_list(self) ->
|
|
938
|
+
def get_columns_list(self) -> list[str]:
|
|
832
939
|
"""
|
|
833
940
|
Returns all model's columns on SQLA properties
|
|
834
941
|
"""
|
|
835
942
|
return list(self.list_properties.keys())
|
|
836
943
|
|
|
837
|
-
def get_user_columns_list(self) ->
|
|
944
|
+
def get_user_columns_list(self) -> list[str]:
|
|
838
945
|
"""
|
|
839
946
|
Returns all model's columns except pk or fk
|
|
840
947
|
"""
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
948
|
+
return [
|
|
949
|
+
col_name
|
|
950
|
+
for col_name in self.get_columns_list()
|
|
951
|
+
if (not self.is_pk(col_name)) and (not self.is_fk(col_name))
|
|
952
|
+
]
|
|
846
953
|
|
|
847
954
|
# TODO get different solution, more integrated with filters
|
|
848
|
-
def get_search_columns_list(self) ->
|
|
849
|
-
ret_lst =
|
|
955
|
+
def get_search_columns_list(self) -> list[str]:
|
|
956
|
+
ret_lst = []
|
|
850
957
|
for col_name in self.get_columns_list():
|
|
851
958
|
if not self.is_relation(col_name):
|
|
852
959
|
tmp_prop = self.get_property_first_col(col_name).name
|
|
@@ -861,34 +968,37 @@ class SQLAInterface(BaseInterface):
|
|
|
861
968
|
ret_lst.append(col_name)
|
|
862
969
|
return ret_lst
|
|
863
970
|
|
|
864
|
-
def get_order_columns_list(self, list_columns:
|
|
971
|
+
def get_order_columns_list(self, list_columns: list[str] = None) -> list[str]:
|
|
865
972
|
"""
|
|
866
|
-
Returns the columns that can be ordered
|
|
973
|
+
Returns the columns that can be ordered.
|
|
867
974
|
|
|
868
975
|
:param list_columns: optional list of columns name, if provided will
|
|
869
976
|
use this list only.
|
|
870
977
|
"""
|
|
871
|
-
ret_lst =
|
|
978
|
+
ret_lst = []
|
|
872
979
|
list_columns = list_columns or self.get_columns_list()
|
|
980
|
+
|
|
873
981
|
for col_name in list_columns:
|
|
874
|
-
if
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
else:
|
|
982
|
+
if self.is_relation(col_name):
|
|
983
|
+
continue
|
|
984
|
+
|
|
985
|
+
if hasattr(self.obj, col_name):
|
|
986
|
+
attribute = getattr(self.obj, col_name)
|
|
987
|
+
if not callable(attribute) or hasattr(attribute, "_col_name"):
|
|
881
988
|
ret_lst.append(col_name)
|
|
989
|
+
else:
|
|
990
|
+
ret_lst.append(col_name)
|
|
991
|
+
|
|
882
992
|
return ret_lst
|
|
883
993
|
|
|
884
|
-
def get_file_column_list(self) ->
|
|
994
|
+
def get_file_column_list(self) -> list[str]:
|
|
885
995
|
return [
|
|
886
996
|
i.name
|
|
887
997
|
for i in self.obj.__mapper__.columns
|
|
888
998
|
if isinstance(i.type, FileColumn)
|
|
889
999
|
]
|
|
890
1000
|
|
|
891
|
-
def get_image_column_list(self) ->
|
|
1001
|
+
def get_image_column_list(self) -> list[str]:
|
|
892
1002
|
return [
|
|
893
1003
|
i.name
|
|
894
1004
|
for i in self.obj.__mapper__.columns
|
|
@@ -899,15 +1009,16 @@ class SQLAInterface(BaseInterface):
|
|
|
899
1009
|
# support for only one col for pk and fk
|
|
900
1010
|
return self.list_properties[col_name].columns[0]
|
|
901
1011
|
|
|
902
|
-
def get_relation_fk(self, col_name: str) ->
|
|
1012
|
+
def get_relation_fk(self, col_name: str) -> Column:
|
|
903
1013
|
# support for only one col for pk and fk
|
|
904
1014
|
return list(self.list_properties[col_name].local_columns)[0]
|
|
905
1015
|
|
|
906
1016
|
def get(
|
|
907
1017
|
self,
|
|
908
|
-
id,
|
|
1018
|
+
id: Any,
|
|
909
1019
|
filters: Optional[Filters] = None,
|
|
910
|
-
select_columns: Optional[
|
|
1020
|
+
select_columns: Optional[list[str]] = None,
|
|
1021
|
+
outer_default_load: bool = False,
|
|
911
1022
|
) -> Optional[Model]:
|
|
912
1023
|
"""
|
|
913
1024
|
Returns the result for a model get, applies filters and supports dotted
|
|
@@ -925,38 +1036,41 @@ class SQLAInterface(BaseInterface):
|
|
|
925
1036
|
else:
|
|
926
1037
|
_filters = Filters(self.filter_converter_class, self)
|
|
927
1038
|
|
|
928
|
-
if self.is_pk_composite():
|
|
1039
|
+
if self.is_pk_composite() and isinstance(pk, Iterable):
|
|
929
1040
|
for _pk, _id in zip(pk, id):
|
|
930
1041
|
_filters.add_filter(_pk, self.FilterEqual, _id)
|
|
931
1042
|
else:
|
|
932
1043
|
_filters.add_filter(pk, self.FilterEqual, id)
|
|
933
1044
|
query = self.session.query(self.obj)
|
|
934
1045
|
item = self.apply_all(
|
|
935
|
-
query,
|
|
1046
|
+
query,
|
|
1047
|
+
_filters,
|
|
1048
|
+
select_columns=select_columns,
|
|
1049
|
+
outer_default_load=outer_default_load,
|
|
936
1050
|
).one_or_none()
|
|
937
1051
|
if item:
|
|
938
1052
|
if hasattr(item, self.obj.__name__):
|
|
939
1053
|
return getattr(item, self.obj.__name__)
|
|
940
1054
|
return item
|
|
941
1055
|
|
|
942
|
-
def get_pk_name(self) -> Optional[
|
|
1056
|
+
def get_pk_name(self) -> Optional[list[str] | str]:
|
|
943
1057
|
"""
|
|
944
1058
|
Get the model primary key column name.
|
|
945
1059
|
"""
|
|
946
1060
|
return self._get_pk_name(self.obj)
|
|
947
1061
|
|
|
948
|
-
def get_pk(self, model: Optional[Type[Model]] = None):
|
|
1062
|
+
def get_pk(self, model: Optional[Type[Model]] = None) -> Model | None:
|
|
949
1063
|
"""
|
|
950
1064
|
Get the model primary key SQLAlchemy column.
|
|
951
1065
|
Will not support composite keys
|
|
952
1066
|
"""
|
|
953
|
-
model_ = model
|
|
1067
|
+
model_ = self.obj if model is None else model
|
|
954
1068
|
pk_name = self._get_pk_name(model_)
|
|
955
1069
|
if pk_name and isinstance(pk_name, str):
|
|
956
1070
|
return getattr(model_, pk_name)
|
|
957
1071
|
return None
|
|
958
1072
|
|
|
959
|
-
def _get_pk_name(self, model: Type[Model]) -> Optional[
|
|
1073
|
+
def _get_pk_name(self, model: Type[Model]) -> Optional[list[str] | str]:
|
|
960
1074
|
pk = [pk.name for pk in model.__mapper__.primary_key]
|
|
961
1075
|
if pk:
|
|
962
1076
|
return pk if self.is_pk_composite() else pk[0]
|