mirror of
https://github.com/apache/superset.git
synced 2026-09-01 21:11:28 +00:00
104 lines
3.8 KiB
Python
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,
|
|
),
|
|
)
|
|
)
|