chore(extensions): unified contribution api and automatic prefixing (#38412)

This commit is contained in:
Ville Brofeldt
2026-03-04 14:51:22 -08:00
committed by GitHub
parent 939e4194c6
commit 0d5827ac42
11 changed files with 492 additions and 102 deletions

View File

@@ -115,22 +115,51 @@ Backend contribution types allow extensions to extend Superset's server-side cap
### REST API Endpoints
Extensions can register custom REST API endpoints under the `/api/v1/extensions/` namespace. This dedicated namespace prevents conflicts with built-in endpoints and provides a clear separation between core and extension functionality.
```json
"backend": {
"entryPoints": ["my_extension.entrypoint"],
"files": ["backend/src/my_extension/**/*.py"]
}
```
The entry point module registers the API with Superset:
Extensions can register custom REST API endpoints under the `/extensions/` namespace. This dedicated namespace prevents conflicts with built-in endpoints and provides a clear separation between core and extension functionality.
```python
from superset_core.api.rest_api import add_extension_api
from .api import MyExtensionAPI
from superset_core.api.rest_api import RestApi, api
from flask_appbuilder.api import expose, protect
add_extension_api(MyExtensionAPI)
@api(
id="my_extension_api",
name="My Extension API",
description="Custom API endpoints for my extension"
)
class MyExtensionAPI(RestApi):
@expose("/hello", methods=("GET",))
@protect()
def hello(self) -> Response:
return self.response(200, result={"message": "Hello from extension!"})
# Import the class in entrypoint.py to register it
from .api import MyExtensionAPI
```
**Note**: The [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator automatically detects context and generates appropriate paths:
- **Extension context**: `/extensions/{publisher}/{name}/` with ID prefixed as `extensions.{publisher}.{name}.{id}`
- **Host context**: `/api/v1/` with original ID
For an extension with publisher `my-org` and name `dataset-tools`, the endpoint above would be accessible at:
```
/extensions/my-org/dataset-tools/hello
```
You can also specify a `resource_name` parameter to add an additional path segment:
```python
@api(
id="analytics_api",
name="Analytics API",
resource_name="analytics" # Adds /analytics to the path
)
class AnalyticsAPI(RestApi):
@expose("/insights", methods=("GET",))
def insights(self):
# This endpoint will be available at:
# /extensions/my-org/dataset-tools/analytics/insights
return self.response(200, result={"insights": []})
```
### MCP Tools and Prompts

View File

@@ -203,32 +203,52 @@ Extension endpoints are registered under a dedicated `/extensions` namespace to
```python
from superset_core.api.models import Database, get_session
from superset_core.api.daos import DatabaseDAO
from superset_core.api.rest_api import add_extension_api
from .api import DatasetReferencesAPI
from superset_core.api.rest_api import RestApi, api
from flask_appbuilder.api import expose, protect
# Register a new extension REST API
add_extension_api(DatasetReferencesAPI)
# Fetch Superset entities via the DAO to apply base filters that filter out entities
# that the user doesn't have access to
databases = DatabaseDAO.find_all()
# ..or apply simple filters on top of base filters
databases = DatabaseDAO.filter_by(uuid=database.uuid)
if not databases:
raise Exception("Database not found")
return databases[0]
# Perform complex queries using SQLAlchemy Query, also filtering out
# inaccessible entities
session = get_session()
databases_query = session.query(Database).filter(
Database.database_name.ilike("%abc%")
@api(
id="dataset_references_api",
name="Dataset References API",
description="API for managing dataset references"
)
return DatabaseDAO.query(databases_query)
class DatasetReferencesAPI(RestApi):
@expose("/datasets", methods=("GET",))
@protect()
def get_datasets(self) -> Response:
"""Get all accessible datasets."""
# Fetch Superset entities via the DAO to apply base filters that filter out entities
# that the user doesn't have access to
databases = DatabaseDAO.find_all()
# ..or apply simple filters on top of base filters
databases = DatabaseDAO.filter_by(uuid=database.uuid)
if not databases:
raise Exception("Database not found")
return self.response(200, result={"databases": databases})
@expose("/search", methods=("GET",))
@protect()
def search_databases(self) -> Response:
"""Search databases with complex queries."""
# Perform complex queries using SQLAlchemy Query, also filtering out
# inaccessible entities
session = get_session()
databases_query = session.query(Database).filter(
Database.database_name.ilike("%abc%")
)
databases = DatabaseDAO.query(databases_query)
return self.response(200, result={"databases": databases})
```
### Automatic Context Detection
The [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator automatically detects whether it's being used in host or extension code:
- **Extension APIs**: Registered under `/extensions/{publisher}/{name}/` with IDs prefixed as `extensions.{publisher}.{name}.{id}`
- **Host APIs**: Registered under `/api/v1/` with original IDs
In the future, we plan to expand the backend APIs to support configuring security models, database engines, SQL Alchemy dialects, etc.
## Development Mode

View File

@@ -38,7 +38,7 @@ Extensions can provide:
- **Custom UI Components**: New panels, views, and interactive elements
- **Commands and Menus**: Custom actions accessible via menus and keyboard shortcuts
- **REST API Endpoints**: Backend services under the `/api/v1/extensions/` namespace
- **REST API Endpoints**: Backend services under the `/extensions/` namespace
- **MCP Tools and Prompts**: AI agent capabilities for enhanced user assistance
## UI Components for Extensions

View File

@@ -129,11 +129,15 @@ The CLI generated a basic `backend/src/superset_extensions/my_org/hello_world/en
```python
from flask import Response
from flask_appbuilder.api import expose, protect, safe
from superset_core.api.rest_api import RestApi
from superset_core.api.rest_api import RestApi, api
@api(
id="hello_world_api",
name="Hello World API",
description="API endpoints for the Hello World extension"
)
class HelloWorldAPI(RestApi):
resource_name = "hello_world"
openapi_spec_tag = "Hello World"
class_permission_name = "hello_world"
@@ -170,25 +174,25 @@ class HelloWorldAPI(RestApi):
**Key points:**
- Extends `RestApi` from `superset_core.api.types.rest_api`
- Uses [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator with automatic context detection
- Extends `RestApi` from `superset_core.api.rest_api`
- Uses Flask-AppBuilder decorators (`@expose`, `@protect`, `@safe`)
- Returns responses using `self.response(status_code, result=data)`
- The endpoint will be accessible at `/extensions/my-org/hello-world/message`
- The endpoint will be accessible at `/extensions/my-org/hello-world/message` (automatic extension context)
- OpenAPI docstrings are crucial - Flask-AppBuilder uses them to automatically generate interactive API documentation at `/swagger/v1`, allowing developers to explore endpoints, understand schemas, and test the API directly from the browser
**Update `backend/src/superset_extensions/my_org/hello_world/entrypoint.py`**
Replace the generated print statement with API registration:
Replace the generated print statement with API import to trigger registration:
```python
from superset_core.api import rest_api
# Importing the API class triggers the @api decorator registration
from .api import HelloWorldAPI
rest_api.add_extension_api(HelloWorldAPI)
print("Hello World extension loaded successfully!")
```
This registers your API with Superset when the extension loads.
The [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator automatically detects extension context and registers your API with proper namespacing.
## Step 5: Create Frontend Component
@@ -328,16 +332,16 @@ const HelloWorldPanel: React.FC = () => {
const [error, setError] = useState<string>('');
useEffect(() => {
const fetchMessage = async () => {
try {
const csrfToken = await authentication.getCSRFToken();
const response = await fetch('/extensions/my-org/hello-world/message', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken!,
},
});
const fetchMessage = async () => {
try {
const csrfToken = await authentication.getCSRFToken();
const response = await fetch('/extensions/my-org/hello-world/message', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken!,
},
});
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
@@ -493,7 +497,7 @@ Superset will extract and validate the extension metadata, load the assets, regi
Here's what happens when your extension loads:
1. **Superset starts**: Reads `extension.json` and loads the backend entrypoint
2. **Backend registration**: `entrypoint.py` registers your API via `rest_api.add_extension_api()`
2. **Backend registration**: `entrypoint.py` imports your API class, triggering the [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator to register it automatically
3. **Frontend loads**: When SQL Lab opens, Superset fetches the remote entry file
4. **Module Federation**: Webpack loads your extension module and resolves `@apache-superset/core` to `window.superset`
5. **Registration**: The module executes at load time, calling `views.registerView` to register your panel