Skip to content

Commit

Permalink
feat(jinja): add advanced temporal functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
villebro committed Sep 3, 2024
1 parent 9c3eb8f commit a5bb398
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 4 deletions.
3 changes: 2 additions & 1 deletion superset/common/query_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
time_range: str | None
to_dttm: datetime | None

def __init__( # pylint: disable=too-many-locals, too-many-arguments
def __init__( # pylint: disable=too-many-locals
self,
*,
annotation_layers: list[dict[str, Any]] | None = None,
Expand Down Expand Up @@ -335,6 +335,7 @@ def to_dict(self) -> dict[str, Any]:
"series_limit_metric": self.series_limit_metric,
"to_dttm": self.to_dttm,
"time_shift": self.time_shift,
"time_range": self.time_range,
}
return query_object_dict

Expand Down
107 changes: 104 additions & 3 deletions superset/jinja_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
# under the License.
"""Defines the templating context for SQL Lab"""

from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime
from functools import lru_cache, partial
from typing import Any, Callable, cast, Optional, TYPE_CHECKING, TypedDict, Union
Expand All @@ -31,13 +34,16 @@
from sqlalchemy.types import String

from superset.commands.dataset.exceptions import DatasetNotFoundError
from superset.constants import LRU_CACHE_MAX_SIZE
from superset.common.utils.time_range_utils import get_since_until_from_time_range
from superset.constants import LRU_CACHE_MAX_SIZE, NO_TIME_RANGE
from superset.exceptions import SupersetTemplateException
from superset.extensions import feature_flag_manager
from superset.sql_parse import Table
from superset.utils import json
from superset.utils.core import (
AdhocFilterClause,
convert_legacy_filters_into_adhoc,
FilterOperator,
get_user_email,
get_user_id,
get_username,
Expand All @@ -62,6 +68,7 @@
"dict",
"tuple",
"set",
"TimeFilter",
)
COLLECTION_TYPES = ("list", "dict", "tuple", "set")

Expand All @@ -77,6 +84,16 @@ class Filter(TypedDict):
val: Union[None, Any, list[Any]]


@dataclass
class TimeFilter:
"""
Container for temporal filter.
"""
from_dttm: str | None
to_dttm: str | None
time_range: str | None


class ExtraCache:
"""
Dummy class that exposes a method used to store additional values used in
Expand All @@ -95,17 +112,23 @@ class ExtraCache:
r").*\}\}"
)

def __init__(
def __init__( # pylint: disable=too-many-arguments
self,
extra_cache_keys: Optional[list[Any]] = None,
applied_filters: Optional[list[str]] = None,
removed_filters: Optional[list[str]] = None,
database: Optional[Database] = None,
dialect: Optional[Dialect] = None,
time_range: Optional[str] = None,
table: Optional[SqlaTable] = None,
):
self.extra_cache_keys = extra_cache_keys
self.applied_filters = applied_filters if applied_filters is not None else []
self.removed_filters = removed_filters if removed_filters is not None else []
self.database = database
self.dialect = dialect
self.time_range = time_range or NO_TIME_RANGE
self.table = table

def current_user_id(self, add_to_cache_keys: bool = True) -> Optional[int]:
"""
Expand Down Expand Up @@ -319,7 +342,6 @@ def get_filters(self, column: str, remove_filter: bool = False) -> list[Filter]:
:return: returns a list of filters
"""
# pylint: disable=import-outside-toplevel
from superset.utils.core import FilterOperator
from superset.views.utils import get_form_data

form_data, _ = get_form_data()
Expand Down Expand Up @@ -354,6 +376,79 @@ def get_filters(self, column: str, remove_filter: bool = False) -> list[Filter]:

return filters

def get_time_filter(
self,
column: str | None = None,
target_type: str | None = None,
remove_filter: bool = False,
) -> TimeFilter:
"""Get the applied time filter with appropriate formatting,
either for a specific column, or whichever time filter is being emitted
from a dashboard.
This is useful if you want to handle time filters inside the virtual dataset,
as by default the time filter is placed on the outer query. This can have
significant performance implications on certain query engines, like Druid.
Usage example::
:param column: Name of the temporal column. Leave undefined to reference the
time range from a Dashboard Native Time Range filter (when present).
:param target_type: The target temporal type. If `column` is defined, will
default to the type of the column. This is used to produce the format
of the `from_dttm` and `to_dttm` properties of the returned `TimeFilter`
object.
:param remove_filter: When set to true, mark the filter as processed,
removing it from the outer query. Useful when a filter should
only apply to the inner query
:return: The corresponding time filter.
"""
# pylint: disable=import-outside-toplevel
from superset.views.utils import get_form_data

form_data, _ = get_form_data()
convert_legacy_filters_into_adhoc(form_data)
merge_extra_filters(form_data)
if column:
flt: AdhocFilterClause | None = next(
(
flt
for flt in form_data.get("adhoc_filters", [])
if flt["operator"] == FilterOperator.TEMPORAL_RANGE
and flt["subject"] == column
),
None,
)
if flt:
if remove_filter:
if column not in self.removed_filters:
self.removed_filters.append(column)
if column not in self.applied_filters:
self.applied_filters.append(column)

time_range = cast(str, flt["comparator"])
if not target_type and self.table:
target_type = self.table.columns_types.get(column)
else:
time_range = self.time_range
else:
time_range = self.time_range

from_dttm, to_dttm = get_since_until_from_time_range(time_range)

def _format_dttm(dttm: datetime | None) -> str | None:
return (
self.database.db_engine_spec.convert_dttm(target_type or "", dttm)
if self.database and dttm
else None
)

return TimeFilter(
from_dttm=_format_dttm(from_dttm),
to_dttm=_format_dttm(to_dttm),
time_range=time_range,
)


def safe_proxy(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
return_value = func(*args, **kwargs)
Expand Down Expand Up @@ -477,6 +572,7 @@ def __init__(
self._schema = query.schema
elif table:
self._schema = table.schema
self._table = table
self._extra_cache_keys = extra_cache_keys
self._applied_filters = applied_filters
self._removed_filters = removed_filters
Expand Down Expand Up @@ -525,7 +621,10 @@ def set_context(self, **kwargs: Any) -> None:
extra_cache_keys=self._extra_cache_keys,
applied_filters=self._applied_filters,
removed_filters=self._removed_filters,
database=self._database,
dialect=self._database.get_dialect(),
table=self._table,
time_range=self._context.get("time_range"),
)

from_dttm = (
Expand All @@ -544,6 +643,7 @@ def set_context(self, **kwargs: Any) -> None:
from_dttm=from_dttm,
to_dttm=to_dttm,
)

self._context.update(
{
"url_param": partial(safe_proxy, extra_cache.url_param),
Expand All @@ -557,6 +657,7 @@ def set_context(self, **kwargs: Any) -> None:
"get_filters": partial(safe_proxy, extra_cache.get_filters),
"dataset": partial(safe_proxy, dataset_macro_with_context),
"metric": partial(safe_proxy, metric_macro),
"get_time_filter": partial(safe_proxy, extra_cache.get_time_filter),
}
)

Expand Down
2 changes: 2 additions & 0 deletions superset/models/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,7 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma
timeseries_limit: Optional[int] = None,
timeseries_limit_metric: Optional[Metric] = None,
time_shift: Optional[str] = None,
time_range: Optional[str] = None,
) -> SqlaQuery:
"""Querying any sqla table from this common interface"""
if granularity not in self.dttm_cols and granularity is not None:
Expand All @@ -1457,6 +1458,7 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma
"time_column": granularity,
"time_grain": time_grain,
"to_dttm": to_dttm.isoformat() if to_dttm else None,
"time_range": time_range,
"table_columns": [col.column_name for col in self.columns],
"filter": filter,
}
Expand Down

0 comments on commit a5bb398

Please sign in to comment.