Source code for operators.postgres.utils.get_tenants
from airflow.models import BaseOperator
from airflow.providers.postgres.hooks.postgres import PostgresHook
from airflow.exceptions import AirflowException
from typing import Any
import json, ast
[docs]
class GetTenantsOperator(BaseOperator):
"""
Operator to retrieve a list of tenant schema names from a Postgres database.
:param postgres_conn_id: Airflow connection id for the Postgres database.
:param additional_exclude_schemas: Optional collection or string of schema names
to exclude in addition to defaults. Accepts list/tuple/set, a JSON string
(e.g. '["public","demo"]'), a Python-literal string (e.g. "['public']"),
or a single plain string (e.g. "public"). This is CaSe SeNsItIvE.
"""
template_fields = ("postgres_conn_id", "additional_exclude_schemas")
ui_color = "#4D6A42"
def __init__(
self,
postgres_conn_id: str,
additional_exclude_schemas: Any = None, # <-- store raw, coerce later
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
self.postgres_conn_id = postgres_conn_id
# Store raw; coerce at execution time after templating has rendered.
self.additional_exclude_schemas = additional_exclude_schemas
@staticmethod
def _coerce_excludes(raw: Any) -> list[str]:
"""Turn any templated input into a list[str]."""
if raw is None:
return []
if isinstance(raw, (list, tuple, set)):
return [str(x) for x in raw if str(x).strip()]
if isinstance(raw, str):
s = raw.strip()
if not s:
return []
# Try JSON then Python literal; otherwise treat as single name
try:
v = json.loads(s)
except Exception:
try:
v = ast.literal_eval(s)
except Exception:
return [s]
if isinstance(v, (list, tuple, set)):
return [str(x) for x in v if str(x).strip()]
return [str(v)]
return [str(raw)]
[docs]
def execute(self, context):
"""Retrieve the list of tenant schema names from the Postgres database."""
user_excludes = self._coerce_excludes(self.additional_exclude_schemas)
# Always exclude information_schema; add any user-provided schemas
base_excludes = {
"information_schema",
"public",
"aws_commons",
"aws_s3",
"test_base",
}
exclude_schemas = tuple(sorted(base_excludes | set(user_excludes)))
self.log.info(
"GetTenantsOperator using excludes=%s (raw=%r)",
exclude_schemas,
self.additional_exclude_schemas,
)
hook = PostgresHook(postgres_conn_id=self.postgres_conn_id)
sql = """
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name NOT IN %(exclude)s
AND schema_name NOT LIKE 'pg_%%'
ORDER BY schema_name ASC;
"""
try:
records = hook.get_records(sql, {"exclude": exclude_schemas})
tenants = [row[0] for row in records]
except Exception as e:
self.log.exception(
"Error retrieving tenant schemas with conn_id '%s': %s",
self.postgres_conn_id,
e,
)
raise AirflowException(
f"Failed to retrieve tenant schemas from Postgres (conn_id='{self.postgres_conn_id}')."
) from e
if not tenants:
raise AirflowException(
f"No tenant schemas found in Postgres (conn_id='{self.postgres_conn_id}')."
)
self.log.info("Found tenant schemas: %s", tenants)
return tenants