Files
superset2/superset/utils/sqlalchemy_events.py
T

104 lines
3.8 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.
"""Typed declarations for SQLAlchemy deletion-listener effects."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
from threading import RLock
from typing import Any
import sqlalchemy as sa
class DeleteListenerEffect(str, Enum):
"""Classify the durable effect of an ORM deletion listener."""
PERSISTENT_RECORD = "persistent_record"
PERMISSION_ARTIFACT = "permission_artifact"
OBSERVATIONAL = "observational"
@dataclass(frozen=True)
class DeleteListenerDeclaration:
"""Describe one supported-root ``after_delete`` listener."""
target: type[Any]
responsibility: str
effect: DeleteListenerEffect
listener: Callable[..., None]
@property
def key(self) -> tuple[type[Any], str]:
"""Return the stable catalog key for this declaration."""
return self.target, self.responsibility
_DELETE_LISTENERS: dict[tuple[type[Any], str], DeleteListenerDeclaration] = {}
_DELETE_LISTENER_LOCK: RLock = RLock()
def register_delete_listener(declaration: DeleteListenerDeclaration) -> None:
"""Register a declared listener idempotently."""
with _DELETE_LISTENER_LOCK:
existing: DeleteListenerDeclaration | None = _DELETE_LISTENERS.get(
declaration.key
)
if existing is not None and existing != declaration:
raise ValueError(
"Conflicting delete-listener declaration: "
f"{declaration.target.__name__}.{declaration.responsibility}"
)
_DELETE_LISTENERS[declaration.key] = declaration
if not sa.event.contains(
declaration.target, "after_delete", declaration.listener
):
sa.event.listen(declaration.target, "after_delete", declaration.listener)
def remove_delete_listener(declaration: DeleteListenerDeclaration) -> None:
"""Remove a declared listener while preserving catalog identity."""
with _DELETE_LISTENER_LOCK:
existing: DeleteListenerDeclaration | None = _DELETE_LISTENERS.get(
declaration.key
)
if existing is not None and existing != declaration:
raise ValueError(
"Conflicting delete-listener declaration: "
f"{declaration.target.__name__}.{declaration.responsibility}"
)
if sa.event.contains(declaration.target, "after_delete", declaration.listener):
sa.event.remove(declaration.target, "after_delete", declaration.listener)
_DELETE_LISTENERS.pop(declaration.key, None)
def declared_delete_listeners() -> tuple[DeleteListenerDeclaration, ...]:
"""Return declared listeners in deterministic order."""
with _DELETE_LISTENER_LOCK:
return tuple(
sorted(
_DELETE_LISTENERS.values(),
key=lambda declaration: (
declaration.target.__module__,
declaration.target.__qualname__,
declaration.responsibility,
),
)
)