Skip to content

Commit c2baba5

Browse files
authored
chore: abstract models and daos into superset-core (#35259)
1 parent c955a5d commit c2baba5

File tree

35 files changed

+948
-431
lines changed

35 files changed

+948
-431
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ cover
3333
.env
3434
.envrc
3535
.idea
36+
.roo
3637
.mypy_cache
3738
.python-version
3839
.tox

docs/developer_portal/extensions/interacting-with-host.md

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ under the License.
2626

2727
Extensions interact with Superset through well-defined, versioned APIs provided by the `@apache-superset/core` (frontend) and `apache-superset-core` (backend) packages. These APIs are designed to be stable, discoverable, and consistent for both built-in and external extensions.
2828

29+
**Note**: The `superset_core.api` module provides abstract classes that are replaced with concrete implementations via dependency injection when Superset initializes. This allows extensions to use the same interfaces as the host application.
30+
2931
**Frontend APIs** (via `@apache-superset/core)`:
3032

3133
The frontend extension APIs in Superset are organized into logical namespaces such as `authentication`, `commands`, `extensions`, `sqlLab`, and others. Each namespace groups related functionality, making it easy for extension authors to discover and use the APIs relevant to their needs. For example, the `sqlLab` namespace provides events and methods specific to SQL Lab, allowing extensions to react to user actions and interact with the SQL Lab environment:
@@ -90,31 +92,38 @@ Backend APIs follow a similar pattern, providing access to Superset's models, se
9092
Extension endpoints are registered under a dedicated `/extensions` namespace to avoid conflicting with built-in endpoints and also because they don't share the same version constraints. By grouping all extension endpoints under `/extensions`, Superset establishes a clear boundary between core and extension functionality, making it easier to manage, document, and secure both types of APIs.
9193

9294
``` python
93-
from superset_core.api import rest_api, models, query
95+
from superset_core.api.models import Database, get_session
96+
from superset_core.api.daos import DatabaseDAO
97+
from superset_core.api.rest_api import add_extension_api
9498
from .api import DatasetReferencesAPI
9599

96100
# Register a new extension REST API
97-
rest_api.add_extension_api(DatasetReferencesAPI)
101+
add_extension_api(DatasetReferencesAPI)
102+
103+
# Fetch Superset entities via the DAO to apply base filters that filter out entities
104+
# that the user doesn't have access to
105+
databases = DatabaseDAO.find_all()
98106

99-
# Access Superset models with simple queries that filter out entities that
100-
# the user doesn't have access to
101-
databases = models.get_databases(id=database_id)
107+
# ..or apply simple filters on top of base filters
108+
databases = DatabaseDAO.filter_by(uuid=database.uuid)
102109
if not databases:
103-
return self.response_404()
110+
raise Exception("Database not found")
104111

105-
database = databases[0]
112+
return databases[0]
106113

107-
# Perform complex queries using SQLAlchemy BaseQuery, also filtering
108-
# out inaccessible entities
109-
session = models.get_session()
110-
db_model = models.get_database_model())
111-
database_query = session.query(db_model.database_name.ilike("%abc%")
112-
databases_containing_abc = models.get_databases(query)
114+
# Perform complex queries using SQLAlchemy Query, also filtering out
115+
# inaccessible entities
116+
session = get_session()
117+
databases_query = session.query(Database).filter(
118+
Database.database_name.ilike("%abc%")
119+
)
120+
return DatabaseDAO.query(databases_query)
113121

114122
# Bypass security model for highly custom use cases
115-
session = models.get_session()
116-
db_model = models.get_database_model())
117-
all_databases_containg_abc = session.query(db_model.database_name.ilike("%abc%").all()
123+
session = get_session()
124+
all_databases_containing_abc = session.query(Database).filter(
125+
Database.database_name.ilike("%abc%")
126+
).all()
118127
```
119128

120129
In the future, we plan to expand the backend APIs to support configuring security models, database engines, SQL Alchemy dialects, etc.

docs/developer_portal/extensions/quick-start.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ The CLI generated a basic `backend/src/hello_world/entrypoint.py`. We'll create
128128
```python
129129
from flask import Response
130130
from flask_appbuilder.api import expose, protect, safe
131-
from superset_core.api.types.rest_api import RestApi
131+
from superset_core.api.rest_api import RestApi
132132

133133

134134
class HelloWorldAPI(RestApi):

superset-core/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ The package is organized into logical modules, each providing specific functiona
4949
from flask import request, Response
5050
from flask_appbuilder.api import expose, permission_name, protect, safe
5151
from superset_core.api import models, query, rest_api
52-
from superset_core.api.types.rest_api import RestApi
52+
from superset_core.api.rest_api import RestApi
5353

5454
class DatasetReferencesAPI(RestApi):
5555
"""Example extension API demonstrating core functionality."""

superset-core/pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ classifiers = [
4343
]
4444
dependencies = [
4545
"flask-appbuilder>=5.0.2,<6",
46+
"sqlalchemy>=1.4.0,<2.0",
47+
"sqlalchemy-utils>=0.38.0",
48+
"sqlglot>=27.15.2, <28",
49+
"typing-extensions>=4.0.0",
4650
]
4751

4852
[project.urls]

superset-core/src/superset_core/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17+
18+
"""
19+
Apache Superset Core - Public API with core functions of Superset
20+
"""

superset-core/src/superset_core/api/__init__.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,3 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17-
18-
from .types.models import CoreModelsApi
19-
from .types.query import CoreQueryApi
20-
from .types.rest_api import CoreRestApi
21-
22-
models: CoreModelsApi
23-
rest_api: CoreRestApi
24-
query: CoreQueryApi
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
"""
19+
Data Access Object API for superset-core.
20+
21+
Provides dependency-injected DAO classes that will be replaced by
22+
host implementations during initialization.
23+
24+
Usage:
25+
from superset_core.api.daos import DatasetDAO, DatabaseDAO
26+
27+
# Use standard BaseDAO methods
28+
datasets = DatasetDAO.find_all()
29+
dataset = DatasetDAO.find_one_or_none(id=123)
30+
DatasetDAO.create(attributes={"name": "New Dataset"})
31+
"""
32+
33+
from abc import ABC, abstractmethod
34+
from typing import Any, ClassVar, Generic, TypeVar
35+
36+
from flask_appbuilder.models.filters import BaseFilter
37+
from sqlalchemy.orm import Query as SQLAQuery
38+
39+
from superset_core.api.models import (
40+
Chart,
41+
CoreModel,
42+
Dashboard,
43+
Database,
44+
Dataset,
45+
KeyValue,
46+
Query,
47+
SavedQuery,
48+
Tag,
49+
User,
50+
)
51+
52+
# Type variable bound to our CoreModel
53+
T = TypeVar("T", bound=CoreModel)
54+
55+
56+
class BaseDAO(Generic[T], ABC):
57+
"""
58+
Abstract base class for DAOs.
59+
60+
This ABC defines the base that all DAOs should implement,
61+
providing consistent CRUD operations across Superset and extensions.
62+
"""
63+
64+
# Due to mypy limitations, we can't have `type[T]` here
65+
model_cls: ClassVar[type[Any] | None]
66+
base_filter: ClassVar[BaseFilter | None]
67+
id_column_name: ClassVar[str]
68+
uuid_column_name: ClassVar[str]
69+
70+
@classmethod
71+
@abstractmethod
72+
def find_all(cls) -> list[T]:
73+
"""Get all entities that fit the base_filter."""
74+
...
75+
76+
@classmethod
77+
@abstractmethod
78+
def find_one_or_none(cls, **filter_by: Any) -> T | None:
79+
"""Get the first entity that fits the base_filter."""
80+
...
81+
82+
@classmethod
83+
@abstractmethod
84+
def create(
85+
cls,
86+
item: T | None = None,
87+
attributes: dict[str, Any] | None = None,
88+
) -> T:
89+
"""Create an object from the specified item and/or attributes."""
90+
...
91+
92+
@classmethod
93+
@abstractmethod
94+
def update(
95+
cls,
96+
item: T | None = None,
97+
attributes: dict[str, Any] | None = None,
98+
) -> T:
99+
"""Update an object from the specified item and/or attributes."""
100+
...
101+
102+
@classmethod
103+
@abstractmethod
104+
def delete(cls, items: list[T]) -> None:
105+
"""Delete the specified items."""
106+
...
107+
108+
@classmethod
109+
@abstractmethod
110+
def query(cls, query: SQLAQuery) -> list[T]:
111+
"""Execute query with base_filter applied."""
112+
...
113+
114+
@classmethod
115+
@abstractmethod
116+
def filter_by(cls, **filter_by: Any) -> list[T]:
117+
"""Get all entries that fit the base_filter."""
118+
...
119+
120+
121+
class DatasetDAO(BaseDAO[Dataset]):
122+
"""
123+
Abstract Dataset DAO interface.
124+
125+
Host implementations will replace this class during initialization
126+
with a concrete implementation providing actual functionality.
127+
"""
128+
129+
# Class variables that will be set by host implementation
130+
model_cls = None
131+
base_filter = None
132+
id_column_name = "id"
133+
uuid_column_name = "uuid"
134+
135+
136+
class DatabaseDAO(BaseDAO[Database]):
137+
"""
138+
Abstract Database DAO interface.
139+
140+
Host implementations will replace this class during initialization
141+
with a concrete implementation providing actual functionality.
142+
"""
143+
144+
# Class variables that will be set by host implementation
145+
model_cls = None
146+
base_filter = None
147+
id_column_name = "id"
148+
uuid_column_name = "uuid"
149+
150+
151+
class ChartDAO(BaseDAO[Chart]):
152+
"""
153+
Abstract Chart DAO interface.
154+
155+
Host implementations will replace this class during initialization
156+
with a concrete implementation providing actual functionality.
157+
"""
158+
159+
# Class variables that will be set by host implementation
160+
model_cls = None
161+
base_filter = None
162+
id_column_name = "id"
163+
uuid_column_name = "uuid"
164+
165+
166+
class DashboardDAO(BaseDAO[Dashboard]):
167+
"""
168+
Abstract Dashboard DAO interface.
169+
170+
Host implementations will replace this class during initialization
171+
with a concrete implementation providing actual functionality.
172+
"""
173+
174+
# Class variables that will be set by host implementation
175+
model_cls = None
176+
base_filter = None
177+
id_column_name = "id"
178+
uuid_column_name = "uuid"
179+
180+
181+
class UserDAO(BaseDAO[User]):
182+
"""
183+
Abstract User DAO interface.
184+
185+
Host implementations will replace this class during initialization
186+
with a concrete implementation providing actual functionality.
187+
"""
188+
189+
# Class variables that will be set by host implementation
190+
model_cls = None
191+
base_filter = None
192+
id_column_name = "id"
193+
194+
195+
class QueryDAO(BaseDAO[Query]):
196+
"""
197+
Abstract Query DAO interface.
198+
199+
Host implementations will replace this class during initialization
200+
with a concrete implementation providing actual functionality.
201+
"""
202+
203+
# Class variables that will be set by host implementation
204+
model_cls = None
205+
base_filter = None
206+
id_column_name = "id"
207+
208+
209+
class SavedQueryDAO(BaseDAO[SavedQuery]):
210+
"""
211+
Abstract SavedQuery DAO interface.
212+
213+
Host implementations will replace this class during initialization
214+
with a concrete implementation providing actual functionality.
215+
"""
216+
217+
# Class variables that will be set by host implementation
218+
model_cls = None
219+
base_filter = None
220+
id_column_name = "id"
221+
222+
223+
class TagDAO(BaseDAO[Tag]):
224+
"""
225+
Abstract Tag DAO interface.
226+
227+
Host implementations will replace this class during initialization
228+
with a concrete implementation providing actual functionality.
229+
"""
230+
231+
# Class variables that will be set by host implementation
232+
model_cls = None
233+
base_filter = None
234+
id_column_name = "id"
235+
236+
237+
class KeyValueDAO(BaseDAO[KeyValue]):
238+
"""
239+
Abstract KeyValue DAO interface.
240+
241+
Host implementations will replace this class during initialization
242+
with a concrete implementation providing actual functionality.
243+
"""
244+
245+
# Class variables that will be set by host implementation
246+
model_cls = None
247+
base_filter = None
248+
id_column_name = "id"
249+
250+
251+
__all__ = [
252+
"BaseDAO",
253+
"DatasetDAO",
254+
"DatabaseDAO",
255+
"ChartDAO",
256+
"DashboardDAO",
257+
"UserDAO",
258+
"QueryDAO",
259+
"SavedQueryDAO",
260+
"TagDAO",
261+
"KeyValueDAO",
262+
]

0 commit comments

Comments
 (0)