mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
319 lines
8.7 KiB
Markdown
319 lines
8.7 KiB
Markdown
---
|
|
title: Overview
|
|
sidebar_position: 1
|
|
---
|
|
|
|
<!--
|
|
Licensed to the Apache Software Foundation (ASF) under one
|
|
or more contributor license agreements. See the NOTICE file
|
|
distributed with this work for additional information
|
|
regarding copyright ownership. The ASF licenses this file
|
|
to you under the Apache License, Version 2.0 (the
|
|
"License"); you may not use this file except in compliance
|
|
with the License. You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing,
|
|
software distributed under the License is distributed on an
|
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
KIND, either express or implied. See the License for the
|
|
specific language governing permissions and limitations
|
|
under the License.
|
|
-->
|
|
|
|
# Backend Style Guidelines
|
|
|
|
This is a list of statements that describe how we do backend development in Superset. While they might not be 100% true for all files in the repo, they represent the gold standard we strive towards for backend quality and style.
|
|
|
|
- We use a monolithic Python/Flask/Flask-AppBuilder backend, with small single-responsibility satellite services where necessary.
|
|
- Files are generally organized by feature or object type. Within each domain, we can have api controllers, models, schemas, commands, and data access objects (DAO).
|
|
- See: [Proposal for Improving Superset's Python Code Organization](https://github.com/apache/superset/issues/9077)
|
|
- API controllers use Marshmallow Schemas to serialize/deserialize data.
|
|
- Authentication and authorization are controlled by the [security manager](https://github.com/apache/superset/blob/master/superset/security/manager).
|
|
- We use Pytest for unit and integration tests. These live in the `tests` directory.
|
|
- We add tests for every new piece of functionality added to the backend.
|
|
- We use pytest fixtures to share setup between tests.
|
|
- We use SQLAlchemy to access both Superset's application database, and users' analytics databases.
|
|
- We make changes backwards compatible whenever possible.
|
|
- If a change cannot be made backwards compatible, it goes into a major release.
|
|
- See: [Proposal For Semantic Versioning](https://github.com/apache/superset/issues/12566)
|
|
- We use Swagger for API documentation, with docs written inline on the API endpoint code.
|
|
- We prefer thin ORM models, putting shared functionality in other utilities.
|
|
- Several linters/checkers are used to maintain consistent code style and type safety: pylint, mypy, black, isort.
|
|
- `__init__.py` files are kept empty to avoid implicit dependencies.
|
|
|
|
## Code Organization
|
|
|
|
### Domain-Driven Structure
|
|
|
|
Organize code by business domain rather than technical layers:
|
|
|
|
```
|
|
superset/
|
|
├── dashboards/
|
|
│ ├── api.py # API endpoints
|
|
│ ├── commands/ # Business logic
|
|
│ ├── dao.py # Data access layer
|
|
│ ├── models.py # Database models
|
|
│ └── schemas.py # Serialization schemas
|
|
├── charts/
|
|
│ ├── api.py
|
|
│ ├── commands/
|
|
│ ├── dao.py
|
|
│ ├── models.py
|
|
│ └── schemas.py
|
|
```
|
|
|
|
### API Controllers
|
|
|
|
Use Flask-AppBuilder with Marshmallow schemas:
|
|
|
|
```python
|
|
from flask_appbuilder.api import BaseApi
|
|
from marshmallow import Schema, fields
|
|
|
|
class DashboardSchema(Schema):
|
|
id = fields.Integer()
|
|
dashboard_title = fields.String(required=True)
|
|
created_on = fields.DateTime(dump_only=True)
|
|
|
|
class DashboardApi(BaseApi):
|
|
resource_name = "dashboard"
|
|
openapi_spec_tag = "Dashboards"
|
|
|
|
@expose("/", methods=["GET"])
|
|
@protect()
|
|
@safe
|
|
def get_list(self) -> Response:
|
|
"""Get a list of dashboards"""
|
|
# Implementation
|
|
```
|
|
|
|
### Commands Pattern
|
|
|
|
Use commands for business logic:
|
|
|
|
```python
|
|
from typing import Optional
|
|
from superset.commands.base import BaseCommand
|
|
from superset.dashboards.dao import DashboardDAO
|
|
|
|
class CreateDashboardCommand(BaseCommand):
|
|
def __init__(self, properties: Dict[str, Any]):
|
|
self._properties = properties
|
|
|
|
def run(self) -> Dashboard:
|
|
self.validate()
|
|
return DashboardDAO.create(self._properties)
|
|
|
|
def validate(self) -> None:
|
|
if not self._properties.get("dashboard_title"):
|
|
raise ValidationError("Title is required")
|
|
```
|
|
|
|
### Data Access Objects (DAOs)
|
|
|
|
See: [DAO Style Guidelines and Best Practices](./backend/dao-style-guidelines)
|
|
|
|
## Testing
|
|
|
|
### Unit Tests
|
|
|
|
```python
|
|
import pytest
|
|
from unittest.mock import patch
|
|
from superset.dashboards.commands.create import CreateDashboardCommand
|
|
|
|
def test_create_dashboard_success():
|
|
properties = {
|
|
"dashboard_title": "Test Dashboard",
|
|
"owners": [1]
|
|
}
|
|
|
|
command = CreateDashboardCommand(properties)
|
|
dashboard = command.run()
|
|
|
|
assert dashboard.dashboard_title == "Test Dashboard"
|
|
|
|
def test_create_dashboard_validation_error():
|
|
properties = {} # Missing required title
|
|
|
|
command = CreateDashboardCommand(properties)
|
|
|
|
with pytest.raises(ValidationError):
|
|
command.run()
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
```python
|
|
import pytest
|
|
from superset.app import create_app
|
|
from superset.extensions import db
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
app = create_app()
|
|
app.config["TESTING"] = True
|
|
with app.app_context():
|
|
db.create_all()
|
|
yield app
|
|
db.drop_all()
|
|
|
|
def test_dashboard_api_create(app, auth_headers):
|
|
with app.test_client() as client:
|
|
response = client.post(
|
|
"/api/v1/dashboard/",
|
|
json={"dashboard_title": "Test Dashboard"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 201
|
|
```
|
|
|
|
## Security
|
|
|
|
### Authorization Decorators
|
|
|
|
```python
|
|
from flask_appbuilder.security.decorators import has_access
|
|
|
|
class DashboardApi(BaseApi):
|
|
@expose("/", methods=["POST"])
|
|
@protect()
|
|
@has_access("can_write", "Dashboard")
|
|
def post(self) -> Response:
|
|
"""Create a new dashboard"""
|
|
# Implementation
|
|
```
|
|
|
|
### Input Validation
|
|
|
|
```python
|
|
from marshmallow import Schema, fields, validate
|
|
|
|
class DashboardSchema(Schema):
|
|
dashboard_title = fields.String(
|
|
required=True,
|
|
validate=validate.Length(min=1, max=500)
|
|
)
|
|
slug = fields.String(
|
|
validate=validate.Regexp(r'^[a-z0-9-]+$')
|
|
)
|
|
```
|
|
|
|
## Database Operations
|
|
|
|
### Use SQLAlchemy ORM
|
|
|
|
```python
|
|
from sqlalchemy import Column, Integer, String, Text
|
|
from superset.models.helpers import AuditMixinNullable
|
|
|
|
class Dashboard(Model, AuditMixinNullable):
|
|
__tablename__ = "dashboards"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
dashboard_title = Column(String(500))
|
|
position_json = Column(Text)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Dashboard {self.dashboard_title}>"
|
|
```
|
|
|
|
### Database Migrations
|
|
|
|
```python
|
|
# migration file
|
|
def upgrade():
|
|
op.add_column(
|
|
"dashboards",
|
|
sa.Column("new_column", sa.String(255), nullable=True)
|
|
)
|
|
|
|
def downgrade():
|
|
op.drop_column("dashboards", "new_column")
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Custom Exceptions
|
|
|
|
```python
|
|
class SupersetException(Exception):
|
|
"""Base exception for Superset"""
|
|
pass
|
|
|
|
class ValidationError(SupersetException):
|
|
"""Raised when validation fails"""
|
|
pass
|
|
|
|
class SecurityException(SupersetException):
|
|
"""Raised when security check fails"""
|
|
pass
|
|
```
|
|
|
|
### Error Responses
|
|
|
|
```python
|
|
from flask import jsonify
|
|
from werkzeug.exceptions import BadRequest
|
|
|
|
@app.errorhandler(ValidationError)
|
|
def handle_validation_error(error):
|
|
return jsonify({
|
|
"message": str(error),
|
|
"error_type": "VALIDATION_ERROR"
|
|
}), 400
|
|
```
|
|
|
|
## Type Hints and Documentation
|
|
|
|
### Use Type Hints
|
|
|
|
```python
|
|
from typing import List, Optional, Dict, Any
|
|
from superset.models.dashboard import Dashboard
|
|
|
|
def get_dashboards_by_owner(owner_id: int) -> List[Dashboard]:
|
|
"""Get all dashboards owned by a specific user"""
|
|
return db.session.query(Dashboard).filter_by(owner_id=owner_id).all()
|
|
|
|
def create_dashboard(properties: Dict[str, Any]) -> Optional[Dashboard]:
|
|
"""Create a new dashboard with the given properties"""
|
|
# Implementation
|
|
```
|
|
|
|
### API Documentation
|
|
|
|
```python
|
|
from flask_appbuilder.api import BaseApi
|
|
from flask_appbuilder.api.schemas import get_list_schema
|
|
|
|
class DashboardApi(BaseApi):
|
|
@expose("/", methods=["GET"])
|
|
@protect()
|
|
@safe
|
|
def get_list(self) -> Response:
|
|
"""Get a list of dashboards
|
|
---
|
|
get:
|
|
description: >-
|
|
Get a list of dashboards
|
|
parameters:
|
|
- in: query
|
|
schema:
|
|
type: integer
|
|
name: page_size
|
|
description: Number of results per page
|
|
responses:
|
|
200:
|
|
description: Success
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/DashboardListResponse'
|
|
"""
|
|
# Implementation
|
|
```
|