Files
superset2/superset/utils/currency.py
Richard Fogaca Nienkotter f4474b2e3e feat: Dynamic currency (#36416)
2026-01-16 21:58:41 -08:00

161 lines
5.3 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.
"""Currency detection utilities for dynamic currency formatting."""
from __future__ import annotations
import logging
from datetime import datetime
from typing import Any, TYPE_CHECKING
from superset.common.db_query_status import QueryStatus
if TYPE_CHECKING:
from superset.explorables.base import Explorable
from superset.superset_typing import QueryObjectDict
from superset.utils.core import QueryObjectFilterClause
logger = logging.getLogger(__name__)
def has_auto_currency_in_column_config(form_data: dict[str, Any] | None) -> bool:
"""
Check if any column in column_config has AUTO currency format.
Used by Table charts which configure currency per-column via column_config,
rather than using top-level currency_format like other chart types.
:param form_data: The form data containing column_config
:return: True if any column has AUTO currency format
"""
if not form_data:
return False
column_config = form_data.get("column_config", {})
if not isinstance(column_config, dict):
return False
for config in column_config.values():
if not isinstance(config, dict):
continue
currency_format = config.get("currencyFormat", {})
if (
isinstance(currency_format, dict)
and currency_format.get("symbol") == "AUTO"
):
return True
return False
def detect_currency_from_df(
df: Any, # pd.DataFrame, but avoiding import for type checking
currency_column: str,
) -> str | None:
"""
Detect currency from an already-fetched dataframe.
Returns the currency code if all data contains a single currency,
or None if multiple currencies are present or column is missing.
:param df: The pandas DataFrame to analyze
:param currency_column: The name of the currency code column
:return: ISO 4217 currency code (e.g., "USD", "EUR") or None
"""
if df is None or df.empty:
return None
if currency_column not in df.columns:
return None
unique_currencies = df[currency_column].dropna().astype(str).str.upper().unique()
if len(unique_currencies) == 1:
return str(unique_currencies[0])
return None
def detect_currency(
datasource: Explorable,
filters: list[QueryObjectFilterClause] | None = None,
granularity: str | None = None,
from_dttm: datetime | None = None,
to_dttm: datetime | None = None,
extras: dict[str, Any] | None = None,
) -> str | None:
"""
Detect currency from filtered data for AUTO mode currency formatting.
Executes a lightweight query to get distinct currency values using the
provided filters. Returns the currency code if all filtered data contains
a single currency, or None if multiple currencies are present.
This utility is used by both the modern QueryContext API and the legacy
viz API to provide consistent currency detection behavior.
:param datasource: The datasource to query
:param filters: List of filter dicts with 'col', 'op', 'val' keys
:param granularity: Optional time granularity column
:param from_dttm: Optional start datetime for time filtering
:param to_dttm: Optional end datetime for time filtering
:param extras: Optional extra query parameters (having, where clauses)
:return: ISO 4217 currency code (e.g., "USD", "EUR") or None
"""
currency_column = getattr(datasource, "currency_code_column", None)
if not currency_column:
return None
datasource_id = getattr(datasource, "id", 0)
query_method = getattr(datasource, "query", None)
if not callable(query_method):
return None
try:
query_obj: QueryObjectDict = {
"granularity": granularity,
"from_dttm": from_dttm,
"to_dttm": to_dttm,
"is_timeseries": False,
"groupby": [currency_column],
"metrics": [],
"filter": filters or [],
"extras": extras or {},
}
result = query_method(query_obj)
if result.status != QueryStatus.SUCCESS or result.df.empty:
return None
if currency_column not in result.df.columns:
return None
unique_currencies = (
result.df[currency_column].dropna().astype(str).str.upper().unique()
)
if len(unique_currencies) == 1:
return str(unique_currencies[0])
return None
except Exception: # pylint: disable=broad-except
logger.warning(
"Failed to detect currency for datasource %s",
datasource_id,
exc_info=True,
)
return None