Files
superset2/superset/extensions/context.py
Evan Rusackas ebc5122af8 feat(extensions): add Tier 3 persistent state storage (#39227)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:22:23 -03:00

142 lines
4.4 KiB
Python

# 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.
"""
Extension Context Management - provides ambient context for extensions.
This module provides a context system using Python's contextvars that allows
extensions to access their context (metadata and scoped resources) via get_context().
The context is set during extension loading and when extension callbacks are invoked.
Uses ContextVar for thread-safe and async-safe context management with automatic
save/restore for nested contexts.
"""
from __future__ import annotations
from contextlib import contextmanager
from contextvars import ContextVar
from typing import Any, Iterator
from superset_core.extensions.types import Manifest
class ExtensionStorage:
"""Extension storage with all available tiers."""
@property
def ephemeral(self) -> Any:
from superset.extensions.storage.ephemeral_state import EphemeralStateImpl
return EphemeralStateImpl
@property
def persistent(self) -> Any:
from superset.extensions.storage.persistent_state_impl import (
PersistentStateImpl,
)
return PersistentStateImpl
class ConcreteExtensionContext:
"""Concrete implementation of ExtensionContext for the host."""
def __init__(self, manifest: Manifest):
self._manifest = manifest
self._storage = ExtensionStorage()
@property
def extension(self) -> Manifest:
"""Extension metadata (new API)."""
return self._manifest
@property
def manifest(self) -> Manifest:
"""Extension manifest (for backward compatibility)."""
return self._manifest
@property
def storage(self) -> ExtensionStorage:
return self._storage
# Context variable for ambient extension context pattern.
# Thread-safe and async-safe via Python's contextvars.
_current_context: ContextVar[ConcreteExtensionContext | None] = ContextVar(
"extension_context", default=None
)
def get_context() -> ConcreteExtensionContext:
"""
Get the current extension's context.
This is the host implementation that replaces the stub in superset_core.
:returns: The current extension's context.
:raises RuntimeError: If called outside of an extension context.
"""
context = _current_context.get()
if context is None:
raise RuntimeError(
"get_context() must be called within an extension context. "
"Ensure this code is being executed during extension loading or "
"within an extension callback."
)
return context
def get_current_extension_context() -> ConcreteExtensionContext | None:
"""Get the currently active extension context, or None if in host code."""
return _current_context.get()
@contextmanager
def use_context(ctx: ConcreteExtensionContext) -> Iterator[None]:
"""
Context manager to set ambient context for extension execution.
Used to establish the ambient context before executing extension code.
The context is automatically restored after execution, supporting nested
context switches.
:param ctx: ExtensionContext to set as the current context
:yields: None
"""
token = _current_context.set(ctx)
try:
yield
finally:
_current_context.reset(token)
@contextmanager
def extension_context(manifest: Manifest) -> Iterator[ConcreteExtensionContext]:
"""
Context manager for setting extension context during loading.
Creates a new ExtensionContext for the given manifest and sets it as
the current context. Supports nested contexts via ContextVar tokens.
:param manifest: The extension manifest
:yields: The created ExtensionContext
"""
ctx = ConcreteExtensionContext(manifest)
with use_context(ctx):
yield ctx