apache-airflow-providers-fab 2.4.4rc1__py3-none-any.whl → 3.0.0rc2__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.

Potentially problematic release.


This version of apache-airflow-providers-fab might be problematic. Click here for more details.

Files changed (25) hide show
  1. airflow/providers/fab/__init__.py +1 -1
  2. airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +2 -2
  3. airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +4 -4
  4. airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +7 -4
  5. airflow/providers/fab/auth_manager/cli_commands/user_command.py +1 -2
  6. airflow/providers/fab/auth_manager/cli_commands/utils.py +7 -3
  7. airflow/providers/fab/auth_manager/fab_auth_manager.py +15 -11
  8. airflow/providers/fab/auth_manager/models/__init__.py +179 -122
  9. airflow/providers/fab/auth_manager/models/db.py +11 -6
  10. airflow/providers/fab/auth_manager/security_manager/override.py +239 -213
  11. airflow/providers/fab/auth_manager/views/user.py +11 -5
  12. airflow/providers/fab/migrations/versions/0001_1_4_0_create_ab_tables_if_missing.py +5 -4
  13. airflow/providers/fab/www/app.py +3 -4
  14. airflow/providers/fab/www/extensions/init_appbuilder.py +26 -39
  15. airflow/providers/fab/www/extensions/init_session.py +2 -2
  16. airflow/providers/fab/www/security_appless.py +6 -1
  17. airflow/providers/fab/www/security_manager.py +4 -14
  18. airflow/providers/fab/www/session.py +26 -3
  19. airflow/providers/fab/www/utils.py +1 -208
  20. {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc2.dist-info}/METADATA +16 -10
  21. {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc2.dist-info}/RECORD +25 -25
  22. {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc2.dist-info}/WHEEL +0 -0
  23. {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  24. {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc2.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +0 -0
  25. {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc2.dist-info}/licenses/NOTICE +0 -0
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "2.4.4"
32
+ __version__ = "3.0.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "3.0.2"
@@ -72,7 +72,7 @@ def get_role(*, role_name: str) -> APIResponse:
72
72
  def get_roles(*, order_by: str = "name", limit: int, offset: int | None = None) -> APIResponse:
73
73
  """Get roles."""
74
74
  security_manager = cast("FabAuthManager", get_auth_manager()).security_manager
75
- session = security_manager.get_session
75
+ session = security_manager.session
76
76
  total_entries = session.scalars(select(func.count(Role.id))).one()
77
77
  direction = desc if order_by.startswith("-") else asc
78
78
  to_replace = {"role_id": "id"}
@@ -99,7 +99,7 @@ def get_roles(*, order_by: str = "name", limit: int, offset: int | None = None)
99
99
  def get_permissions(*, limit: int, offset: int | None = None) -> APIResponse:
100
100
  """Get permissions."""
101
101
  security_manager = cast("FabAuthManager", get_auth_manager()).security_manager
102
- session = security_manager.get_session
102
+ session = security_manager.session
103
103
  total_entries = session.scalars(select(func.count(Action.id))).one()
104
104
  query = select(Action)
105
105
  actions = session.scalars(query.offset(offset).limit(limit)).all()
@@ -56,10 +56,10 @@ def get_user(*, username: str) -> APIResponse:
56
56
 
57
57
  @requires_access_custom_view("GET", permissions.RESOURCE_USER)
58
58
  @format_parameters({"limit": check_limit})
59
- def get_users(*, limit: int, order_by: str = "id", offset: str | None = None) -> APIResponse:
59
+ def get_users(*, limit: int, order_by: str = "id", offset: int | None = None) -> APIResponse:
60
60
  """Get users."""
61
61
  security_manager = cast("FabAuthManager", get_auth_manager()).security_manager
62
- session = security_manager.get_session
62
+ session = security_manager.session
63
63
  total_entries = session.execute(select(func.count(User.id))).scalar()
64
64
  direction = desc if order_by.startswith("-") else asc
65
65
  to_replace = {"user_id": "id"}
@@ -212,7 +212,7 @@ def delete_user(*, username: str) -> APIResponse:
212
212
  raise NotFound(title="User not found", detail=detail)
213
213
 
214
214
  user.roles = [] # Clear foreign keys on this user first.
215
- security_manager.get_session.delete(user)
216
- security_manager.get_session.commit()
215
+ security_manager.session.delete(user)
216
+ security_manager.session.commit()
217
217
 
218
218
  return NoContent, HTTPStatus.NO_CONTENT
@@ -23,6 +23,7 @@ from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_
23
23
  from airflow.configuration import conf
24
24
  from airflow.providers.fab.auth_manager.api_fastapi.datamodels.login import LoginBody, LoginResponse
25
25
  from airflow.providers.fab.auth_manager.api_fastapi.services.login import FABAuthManagerLogin
26
+ from airflow.providers.fab.auth_manager.cli_commands.utils import get_application_builder
26
27
 
27
28
  login_router = AirflowRouter(tags=["FabAuthManager"])
28
29
 
@@ -35,7 +36,8 @@ login_router = AirflowRouter(tags=["FabAuthManager"])
35
36
  )
36
37
  def create_token(body: LoginBody) -> LoginResponse:
37
38
  """Generate a new API token."""
38
- return FABAuthManagerLogin.create_token(body=body)
39
+ with get_application_builder():
40
+ return FABAuthManagerLogin.create_token(body=body)
39
41
 
40
42
 
41
43
  @login_router.post(
@@ -46,6 +48,7 @@ def create_token(body: LoginBody) -> LoginResponse:
46
48
  )
47
49
  def create_token_cli(body: LoginBody) -> LoginResponse:
48
50
  """Generate a new CLI API token."""
49
- return FABAuthManagerLogin.create_token(
50
- body=body, expiration_time_in_seconds=conf.getint("api_auth", "jwt_cli_expiration_time")
51
- )
51
+ with get_application_builder():
52
+ return FABAuthManagerLogin.create_token(
53
+ body=body, expiration_time_in_seconds=conf.getint("api_auth", "jwt_cli_expiration_time")
54
+ )
@@ -144,9 +144,8 @@ def users_delete(args):
144
144
  @providers_configuration_loaded
145
145
  def users_manage_role(args, remove=False):
146
146
  """Delete or appends user roles."""
147
- user = _find_user(args)
148
-
149
147
  with get_application_builder() as appbuilder:
148
+ user = _find_user(args)
150
149
  role = appbuilder.sm.find_role(args.role)
151
150
  if not role:
152
151
  valid_roles = appbuilder.sm.get_all_roles()
@@ -25,6 +25,7 @@ from os.path import isabs
25
25
  from typing import TYPE_CHECKING
26
26
 
27
27
  from flask import Flask
28
+ from flask_sqlalchemy import SQLAlchemy
28
29
  from sqlalchemy.engine import make_url
29
30
 
30
31
  import airflow
@@ -39,11 +40,11 @@ if TYPE_CHECKING:
39
40
 
40
41
 
41
42
  @cache
42
- def _return_appbuilder(app: Flask) -> AirflowAppBuilder:
43
+ def _return_appbuilder(app: Flask, db) -> AirflowAppBuilder:
43
44
  """Return an appbuilder instance for the given app."""
44
45
  init_appbuilder(app, enable_plugins=False)
45
46
  init_plugins(app)
46
- init_airflow_session_interface(app)
47
+ init_airflow_session_interface(app, db)
47
48
  return app.appbuilder # type: ignore[attr-defined]
48
49
 
49
50
 
@@ -63,4 +64,7 @@ def get_application_builder() -> Generator[AirflowAppBuilder, None, None]:
63
64
  "Please use absolute path such as `sqlite:////tmp/airflow.db`."
64
65
  )
65
66
  flask_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
66
- yield _return_appbuilder(flask_app)
67
+
68
+ db = SQLAlchemy(flask_app)
69
+ yield _return_appbuilder(flask_app, db)
70
+ db.engine.dispose(close=True)
@@ -26,7 +26,7 @@ from urllib.parse import urljoin
26
26
  import packaging.version
27
27
  from connexion import FlaskApi
28
28
  from fastapi import FastAPI
29
- from flask import Blueprint, g
29
+ from flask import Blueprint, current_app, g
30
30
  from sqlalchemy import select
31
31
  from sqlalchemy.orm import Session, joinedload
32
32
  from starlette.middleware.wsgi import WSGIMiddleware
@@ -282,7 +282,7 @@ class FabAuthManager(BaseAuthManager[User]):
282
282
 
283
283
  def deserialize_user(self, token: dict[str, Any]) -> User:
284
284
  with create_session() as session:
285
- return session.get(User, int(token["sub"]))
285
+ return session.scalars(select(User).where(User.id == int(token["sub"]))).one()
286
286
 
287
287
  def serialize_user(self, user: User) -> dict[str, Any]:
288
288
  return {"sub": str(user.id)}
@@ -292,7 +292,7 @@ class FabAuthManager(BaseAuthManager[User]):
292
292
  user = self.get_user()
293
293
  return (
294
294
  self.appbuilder
295
- and self.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None)
295
+ and self.appbuilder.app.config.get("AUTH_ROLE_PUBLIC", None)
296
296
  or (not user.is_anonymous and user.is_active)
297
297
  )
298
298
 
@@ -470,14 +470,18 @@ class FabAuthManager(BaseAuthManager[User]):
470
470
  return {dag.dag_id for dag in session.execute(select(DagModel.dag_id))}
471
471
  if isinstance(user, AnonymousUser):
472
472
  return set()
473
- user_query = session.scalar(
474
- select(User)
475
- .options(
476
- joinedload(User.roles)
477
- .subqueryload(Role.permissions)
478
- .options(joinedload(Permission.action), joinedload(Permission.resource))
473
+ user_query = (
474
+ session.scalars(
475
+ select(User)
476
+ .options(
477
+ joinedload(User.roles)
478
+ .subqueryload(Role.permissions)
479
+ .options(joinedload(Permission.action), joinedload(Permission.resource))
480
+ )
481
+ .where(User.id == user.id)
479
482
  )
480
- .where(User.id == user.id)
483
+ .unique()
484
+ .one()
481
485
  )
482
486
  roles = user_query.roles
483
487
 
@@ -547,7 +551,7 @@ class FabAuthManager(BaseAuthManager[User]):
547
551
  if not self.appbuilder:
548
552
  raise AirflowException("AppBuilder is not initialized.")
549
553
 
550
- sm_from_config = self.appbuilder.get_app.config.get("SECURITY_MANAGER_CLASS")
554
+ sm_from_config = current_app.config.get("SECURITY_MANAGER_CLASS")
551
555
  if sm_from_config:
552
556
  if not issubclass(sm_from_config, FabAirflowSecurityManagerOverride):
553
557
  raise AirflowConfigException(
@@ -23,9 +23,8 @@ import datetime
23
23
  # Copyright 2013, Daniel Vaz Gaspar
24
24
  from typing import TYPE_CHECKING
25
25
 
26
- import packaging.version
27
26
  from flask import current_app, g
28
- from flask_appbuilder.models.sqla import Model
27
+ from flask_appbuilder import Model
29
28
  from sqlalchemy import (
30
29
  Boolean,
31
30
  Column,
@@ -33,7 +32,7 @@ from sqlalchemy import (
33
32
  ForeignKey,
34
33
  Index,
35
34
  Integer,
36
- MetaData,
35
+ Sequence,
37
36
  String,
38
37
  Table,
39
38
  UniqueConstraint,
@@ -41,11 +40,19 @@ from sqlalchemy import (
41
40
  func,
42
41
  select,
43
42
  )
44
- from sqlalchemy.orm import backref, declared_attr, registry, relationship
43
+ from sqlalchemy.orm import Mapped, backref, declared_attr, relationship
45
44
 
46
- from airflow import __version__ as airflow_version
47
45
  from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
48
- from airflow.models.base import _get_schema, naming_convention
46
+
47
+ try:
48
+ from sqlalchemy.orm import mapped_column
49
+ except ImportError:
50
+ # fallback for SQLAlchemy < 2.0
51
+ def mapped_column(*args, **kwargs):
52
+ from sqlalchemy import Column
53
+
54
+ return Column(*args, **kwargs)
55
+
49
56
 
50
57
  if TYPE_CHECKING:
51
58
  try:
@@ -57,25 +64,88 @@ if TYPE_CHECKING:
57
64
  Compatibility note: The models in this file are duplicated from Flask AppBuilder.
58
65
  """
59
66
 
60
- metadata = MetaData(schema=_get_schema(), naming_convention=naming_convention)
61
- mapper_registry = registry(metadata=metadata)
67
+ assoc_group_role = Table(
68
+ "ab_group_role",
69
+ Model.metadata,
70
+ Column(
71
+ "id",
72
+ Integer,
73
+ Sequence("ab_group_role_id_seq", start=1, increment=1, minvalue=1, cycle=False),
74
+ primary_key=True,
75
+ ),
76
+ Column("group_id", Integer, ForeignKey("ab_group.id", ondelete="CASCADE")),
77
+ Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
78
+ UniqueConstraint("group_id", "role_id"),
79
+ Index("idx_group_id", "group_id"),
80
+ Index("idx_group_role_id", "role_id"),
81
+ )
62
82
 
63
- if packaging.version.parse(packaging.version.parse(airflow_version).base_version) >= packaging.version.parse(
64
- "3.0.0"
65
- ):
66
- Model.metadata = metadata
67
- else:
68
- from airflow.models.base import Base
83
+ assoc_permission_role = Table(
84
+ "ab_permission_view_role",
85
+ Model.metadata,
86
+ Column(
87
+ "id",
88
+ Integer,
89
+ Sequence(
90
+ "ab_permission_view_role_id_seq",
91
+ start=1,
92
+ increment=1,
93
+ minvalue=1,
94
+ cycle=False,
95
+ ),
96
+ primary_key=True,
97
+ ),
98
+ Column(
99
+ "permission_view_id",
100
+ Integer,
101
+ ForeignKey("ab_permission_view.id", ondelete="CASCADE"),
102
+ ),
103
+ Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
104
+ UniqueConstraint("permission_view_id", "role_id"),
105
+ Index("idx_permission_view_id", "permission_view_id"),
106
+ Index("idx_role_id", "role_id"),
107
+ )
108
+
109
+ assoc_user_role = Table(
110
+ "ab_user_role",
111
+ Model.metadata,
112
+ Column(
113
+ "id",
114
+ Integer,
115
+ Sequence("ab_user_role_id_seq", start=1, increment=1, minvalue=1, cycle=False),
116
+ primary_key=True,
117
+ ),
118
+ Column("user_id", Integer, ForeignKey("ab_user.id", ondelete="CASCADE")),
119
+ Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
120
+ UniqueConstraint("user_id", "role_id"),
121
+ )
69
122
 
70
- Model.metadata = Base.metadata
123
+ assoc_user_group = Table(
124
+ "ab_user_group",
125
+ Model.metadata,
126
+ Column(
127
+ "id",
128
+ Integer,
129
+ Sequence("ab_user_group_id_seq", start=1, increment=1, minvalue=1, cycle=False),
130
+ primary_key=True,
131
+ ),
132
+ Column("user_id", Integer, ForeignKey("ab_user.id", ondelete="CASCADE")),
133
+ Column("group_id", Integer, ForeignKey("ab_group.id", ondelete="CASCADE")),
134
+ UniqueConstraint("user_id", "group_id"),
135
+ )
71
136
 
72
137
 
73
138
  class Action(Model):
74
139
  """Represents permission actions such as `can_read`."""
75
140
 
76
141
  __tablename__ = "ab_permission"
77
- id = Column(Integer, primary_key=True)
78
- name = Column(String(100), unique=True, nullable=False)
142
+
143
+ id: Mapped[int] = mapped_column(
144
+ Integer,
145
+ Sequence("ab_permission_id_seq", start=1, increment=1, minvalue=1, cycle=False),
146
+ primary_key=True,
147
+ )
148
+ name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
79
149
 
80
150
  def __repr__(self):
81
151
  return self.name
@@ -85,8 +155,13 @@ class Resource(Model):
85
155
  """Represents permission object such as `User` or `Dag`."""
86
156
 
87
157
  __tablename__ = "ab_view_menu"
88
- id = Column(Integer, primary_key=True)
89
- name = Column(String(250), unique=True, nullable=False)
158
+
159
+ id: Mapped[int] = mapped_column(
160
+ Integer,
161
+ Sequence("ab_view_menu_id_seq", start=1, increment=1, minvalue=1, cycle=False),
162
+ primary_key=True,
163
+ )
164
+ name: Mapped[str] = mapped_column(String(250), unique=True, nullable=False)
90
165
 
91
166
  def __eq__(self, other):
92
167
  return (isinstance(other, self.__class__)) and (self.name == other.name)
@@ -98,50 +173,18 @@ class Resource(Model):
98
173
  return self.name
99
174
 
100
175
 
101
- assoc_permission_role = Table(
102
- "ab_permission_view_role",
103
- Model.metadata,
104
- Column("id", Integer, primary_key=True),
105
- Column(
106
- "permission_view_id",
107
- Integer,
108
- ForeignKey("ab_permission_view.id", ondelete="CASCADE"),
109
- ),
110
- Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
111
- UniqueConstraint("permission_view_id", "role_id"),
112
- )
113
-
114
- assoc_user_group = Table(
115
- "ab_user_group",
116
- Model.metadata,
117
- Column("id", Integer, primary_key=True),
118
- Column("user_id", Integer, ForeignKey("ab_user.id", ondelete="CASCADE")),
119
- Column("group_id", Integer, ForeignKey("ab_group.id", ondelete="CASCADE")),
120
- UniqueConstraint("user_id", "group_id"),
121
- Index("idx_user_id", "user_id"),
122
- Index("idx_user_group_id", "group_id"),
123
- )
124
-
125
- assoc_group_role = Table(
126
- "ab_group_role",
127
- Model.metadata,
128
- Column("id", Integer, primary_key=True),
129
- Column("group_id", Integer, ForeignKey("ab_group.id", ondelete="CASCADE")),
130
- Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
131
- UniqueConstraint("group_id", "role_id"),
132
- Index("idx_group_id", "group_id"),
133
- Index("idx_group_role_id", "role_id"),
134
- )
135
-
136
-
137
176
  class Role(Model):
138
177
  """Represents a user role to which permissions can be assigned."""
139
178
 
140
179
  __tablename__ = "ab_role"
141
180
 
142
- id = Column(Integer, primary_key=True)
143
- name = Column(String(64), unique=True, nullable=False)
144
- permissions = relationship(
181
+ id: Mapped[int] = mapped_column(
182
+ Integer,
183
+ Sequence("ab_role_id_seq", start=1, increment=1, minvalue=1, cycle=False),
184
+ primary_key=True,
185
+ )
186
+ name: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
187
+ permissions: Mapped[list[Permission]] = relationship(
145
188
  "Permission",
146
189
  secondary=assoc_permission_role,
147
190
  backref="role",
@@ -153,93 +196,100 @@ class Role(Model):
153
196
  return self.name
154
197
 
155
198
 
156
- class Group(Model):
157
- """Represents a user group."""
158
-
159
- __tablename__ = "ab_group"
160
-
161
- id = Column(Integer, primary_key=True)
162
- name = Column(String(100), unique=True, nullable=False)
163
- label = Column(String(150))
164
- description = Column(String(512))
165
- users = relationship("User", secondary=assoc_user_group, backref="groups", passive_deletes=True)
166
- roles = relationship("Role", secondary=assoc_group_role, backref="groups", passive_deletes=True)
167
-
168
- def __repr__(self):
169
- return self.name
170
-
171
-
172
199
  class Permission(Model):
173
200
  """Permission pair comprised of an Action + Resource combo."""
174
201
 
175
202
  __tablename__ = "ab_permission_view"
176
203
  __table_args__ = (UniqueConstraint("permission_id", "view_menu_id"),)
177
- id = Column(Integer, primary_key=True)
178
- action_id = Column("permission_id", Integer, ForeignKey("ab_permission.id"))
179
- action = relationship(
180
- "Action",
181
- uselist=False,
182
- lazy="joined",
183
- )
184
- resource_id = Column("view_menu_id", Integer, ForeignKey("ab_view_menu.id"))
185
- resource = relationship(
186
- "Resource",
187
- uselist=False,
188
- lazy="joined",
204
+ id: Mapped[int] = mapped_column(
205
+ Integer,
206
+ Sequence("ab_permission_view_id_seq", start=1, increment=1, minvalue=1, cycle=False),
207
+ primary_key=True,
189
208
  )
209
+ action_id: Mapped[int] = mapped_column("permission_id", Integer, ForeignKey("ab_permission.id"))
210
+ action: Mapped[Action] = relationship("Action", lazy="joined", uselist=False)
211
+ resource_id: Mapped[int] = mapped_column("view_menu_id", Integer, ForeignKey("ab_view_menu.id"))
212
+ resource: Mapped[Resource] = relationship("Resource", lazy="joined", uselist=False)
190
213
 
191
214
  def __repr__(self):
192
- return str(self.action).replace("_", " ") + " on " + str(self.resource)
215
+ return str(self.action).replace("_", " ") + f" on {str(self.resource)}"
193
216
 
194
217
 
195
- assoc_user_role = Table(
196
- "ab_user_role",
197
- Model.metadata,
198
- Column("id", Integer, primary_key=True),
199
- Column("user_id", Integer, ForeignKey("ab_user.id", ondelete="CASCADE")),
200
- Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
201
- UniqueConstraint("user_id", "role_id"),
202
- )
218
+ class Group(Model):
219
+ """Represents an Airflow user group."""
220
+
221
+ __tablename__ = "ab_group"
222
+
223
+ id: Mapped[int] = mapped_column(
224
+ Integer,
225
+ Sequence("ab_group_id_seq", start=1, increment=1, minvalue=1, cycle=False),
226
+ primary_key=True,
227
+ )
228
+ name: Mapped[str] = Column(String(100), unique=True, nullable=False)
229
+ label: Mapped[str] = Column(String(150))
230
+ description: Mapped[str] = Column(String(512))
231
+ users: Mapped[list[User]] = relationship(
232
+ "User", secondary=assoc_user_group, backref="groups", passive_deletes=True
233
+ )
234
+ roles: Mapped[list[Role]] = relationship(
235
+ "Role", secondary=assoc_group_role, backref="groups", passive_deletes=True
236
+ )
237
+
238
+ def __repr__(self):
239
+ return self.name
203
240
 
204
241
 
205
242
  class User(Model, BaseUser):
206
243
  """Represents an Airflow user which has roles assigned to it."""
207
244
 
208
245
  __tablename__ = "ab_user"
209
- id = Column(Integer, primary_key=True)
210
- first_name = Column(String(256), nullable=False)
211
- last_name = Column(String(256), nullable=False)
212
- username = Column(
246
+
247
+ id: Mapped[int] = mapped_column(
248
+ Integer,
249
+ Sequence("ab_user_id_seq", start=1, increment=1, minvalue=1, cycle=False),
250
+ primary_key=True,
251
+ )
252
+ first_name: Mapped[str] = mapped_column(String(64), nullable=False)
253
+ last_name: Mapped[str] = mapped_column(String(64), nullable=False)
254
+ username: Mapped[str] = mapped_column(
213
255
  String(512).with_variant(String(512, collation="NOCASE"), "sqlite"), unique=True, nullable=False
214
256
  )
215
- password = Column(String(256))
216
- active = Column(Boolean, default=True)
217
- email = Column(String(512), unique=True, nullable=False)
218
- last_login = Column(DateTime)
219
- login_count = Column(Integer)
220
- fail_login_count = Column(Integer)
221
- roles = relationship(
222
- "Role", secondary=assoc_user_role, backref="user", lazy="selectin", passive_deletes=True
257
+ password: Mapped[str | None] = mapped_column(String(256))
258
+ active: Mapped[bool | None] = mapped_column(Boolean, default=True)
259
+ email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False)
260
+ last_login: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True)
261
+ login_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
262
+ fail_login_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
263
+ roles: Mapped[list[Role]] = relationship(
264
+ "Role",
265
+ secondary=assoc_user_role,
266
+ backref="user",
267
+ lazy="selectin",
268
+ passive_deletes=True,
269
+ )
270
+ created_on: Mapped[datetime.datetime | None] = mapped_column(
271
+ DateTime, default=lambda: datetime.datetime.now(), nullable=True
272
+ )
273
+ changed_on: Mapped[datetime.datetime | None] = mapped_column(
274
+ DateTime, default=lambda: datetime.datetime.now(), nullable=True
223
275
  )
224
- created_on = Column(DateTime, default=datetime.datetime.now, nullable=True)
225
- changed_on = Column(DateTime, default=datetime.datetime.now, nullable=True)
226
276
 
227
277
  @declared_attr
228
- def created_by_fk(self):
278
+ def created_by_fk(self) -> Column:
229
279
  return Column(Integer, ForeignKey("ab_user.id"), default=self.get_user_id, nullable=True)
230
280
 
231
281
  @declared_attr
232
- def changed_by_fk(self):
282
+ def changed_by_fk(self) -> Column:
233
283
  return Column(Integer, ForeignKey("ab_user.id"), default=self.get_user_id, nullable=True)
234
284
 
235
- created_by = relationship(
285
+ created_by: Mapped[User] = relationship(
236
286
  "User",
237
287
  backref=backref("created", uselist=True),
238
288
  remote_side=[id],
239
289
  primaryjoin="User.created_by_fk == User.id",
240
290
  uselist=False,
241
291
  )
242
- changed_by = relationship(
292
+ changed_by: Mapped[User] = relationship(
243
293
  "User",
244
294
  backref=backref("changed", uselist=True),
245
295
  remote_side=[id],
@@ -274,7 +324,7 @@ class User(Model, BaseUser):
274
324
  if current_app:
275
325
  sm = current_app.appbuilder.sm
276
326
  self._perms: set[tuple[str, str]] = set(
277
- sm.get_session.execute(
327
+ sm.session.execute(
278
328
  select(sm.action_model.name, sm.resource_model.name)
279
329
  .join(sm.permission_model.action)
280
330
  .join(sm.permission_model.resource)
@@ -307,16 +357,23 @@ class RegisterUser(Model):
307
357
  """Represents a user registration."""
308
358
 
309
359
  __tablename__ = "ab_register_user"
310
- id = Column(Integer, primary_key=True)
311
- first_name = Column(String(256), nullable=False)
312
- last_name = Column(String(256), nullable=False)
313
- username = Column(
360
+
361
+ id = mapped_column(
362
+ Integer,
363
+ Sequence("ab_register_user_id_seq", start=1, increment=1, minvalue=1, cycle=False),
364
+ primary_key=True,
365
+ )
366
+ first_name: Mapped[str] = mapped_column(String(64), nullable=False)
367
+ last_name: Mapped[str] = mapped_column(String(64), nullable=False)
368
+ username: Mapped[str] = mapped_column(
314
369
  String(512).with_variant(String(512, collation="NOCASE"), "sqlite"), unique=True, nullable=False
315
370
  )
316
- password = Column(String(256))
317
- email = Column(String(512), nullable=False)
318
- registration_date = Column(DateTime, default=datetime.datetime.now, nullable=True)
319
- registration_hash = Column(String(256))
371
+ password: Mapped[str | None] = mapped_column(String(256))
372
+ email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False)
373
+ registration_date: Mapped[datetime.datetime | None] = mapped_column(
374
+ DateTime, default=lambda: datetime.datetime.now(), nullable=True
375
+ )
376
+ registration_hash: Mapped[str | None] = mapped_column(String(256))
320
377
 
321
378
 
322
379
  @event.listens_for(User.__table__, "before_create")
@@ -18,9 +18,10 @@ from __future__ import annotations
18
18
 
19
19
  from pathlib import Path
20
20
 
21
+ from flask_appbuilder import Model
22
+
21
23
  from airflow import settings
22
24
  from airflow.exceptions import AirflowException
23
- from airflow.providers.fab.auth_manager.models import metadata
24
25
  from airflow.utils.db import _offline_migration, print_happy_cat
25
26
  from airflow.utils.db_manager import BaseDBManager
26
27
 
@@ -41,14 +42,14 @@ def _get_flask_db(sql_database_uri):
41
42
  flask_app.config["SQLALCHEMY_DATABASE_URI"] = sql_database_uri
42
43
  flask_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
43
44
  db = SQLAlchemy(flask_app)
44
- AirflowDatabaseSessionInterface(app=flask_app, db=db, table="session", key_prefix="")
45
- return db
45
+ AirflowDatabaseSessionInterface(app=flask_app, client=db, table="session", key_prefix="")
46
+ return db, flask_app
46
47
 
47
48
 
48
49
  class FABDBManager(BaseDBManager):
49
50
  """Manages FAB database."""
50
51
 
51
- metadata = metadata
52
+ metadata = Model.metadata
52
53
  version_table_name = "alembic_version_fab"
53
54
  migration_dir = (PACKAGE_DIR / "migrations").as_posix()
54
55
  alembic_file = (PACKAGE_DIR / "alembic.ini").as_posix()
@@ -57,7 +58,9 @@ class FABDBManager(BaseDBManager):
57
58
 
58
59
  def create_db_from_orm(self):
59
60
  super().create_db_from_orm()
60
- _get_flask_db(settings.SQL_ALCHEMY_CONN).create_all()
61
+ db, flask_app = _get_flask_db(settings.SQL_ALCHEMY_CONN)
62
+ with flask_app.app_context():
63
+ db.create_all()
61
64
 
62
65
  def reset_to_2_x(self):
63
66
  self.create_db_from_orm()
@@ -126,4 +129,6 @@ class FABDBManager(BaseDBManager):
126
129
 
127
130
  def drop_tables(self, connection):
128
131
  super().drop_tables(connection)
129
- _get_flask_db(settings.SQL_ALCHEMY_CONN).drop_all()
132
+ db, flask_app = _get_flask_db(settings.SQL_ALCHEMY_CONN)
133
+ with flask_app.app_context():
134
+ db.drop_all()