From 736695de7c16bb2d66577944ce2ef99fa645c33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kr=C4=8Dma?= Date: Mon, 3 Aug 2026 12:13:39 +0200 Subject: [PATCH] feat: add new granularities JIRA: CQ-2758 risk: low --- .../gooddata-dbt/src/gooddata_dbt/dbt/base.py | 4 ++ .../execution-context/date-granularity.json | 4 ++ .../src/gooddata_pandas/arrow_convertor.py | 3 +- .../gooddata-pandas/tests/utils/test_utils.py | 6 +++ .../ldm_extension/input_processor.py | 4 ++ .../src/gooddata_sdk/compute/model/filter.py | 4 ++ .../src/gooddata_sdk/type_converter.py | 2 +- .../src/gooddata_sdk/visualization.py | 4 ++ .../tests/compute_model/test_date_filters.py | 37 +++++++++++++++++++ .../gooddata-sdk/tests/test_type_converter.py | 4 ++ 10 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/gooddata-dbt/src/gooddata_dbt/dbt/base.py b/packages/gooddata-dbt/src/gooddata_dbt/dbt/base.py index 930ab770a..bf99cad2b 100644 --- a/packages/gooddata-dbt/src/gooddata_dbt/dbt/base.py +++ b/packages/gooddata-dbt/src/gooddata_dbt/dbt/base.py @@ -47,9 +47,13 @@ class GoodDataSortDirection(Enum): "FISCAL_YEAR", ] TIMESTAMP_GRANULARITIES = [ + "SECOND", "MINUTE", "HOUR", + "SECOND_OF_MINUTE", + "SECOND_OF_DAY", "MINUTE_OF_HOUR", + "MINUTE_OF_DAY", "HOUR_OF_DAY", ] T = TypeVar("T", bound="Base") diff --git a/packages/gooddata-flexconnect/json_schemas/execution-context/date-granularity.json b/packages/gooddata-flexconnect/json_schemas/execution-context/date-granularity.json index 51948810a..a401dff32 100644 --- a/packages/gooddata-flexconnect/json_schemas/execution-context/date-granularity.json +++ b/packages/gooddata-flexconnect/json_schemas/execution-context/date-granularity.json @@ -5,6 +5,7 @@ "description": "All the supported granularities of the date attributes.", "enum": [ "TIMESTAMP", + "SECOND", "MINUTE", "HOUR", "DAY", @@ -12,7 +13,10 @@ "MONTH", "QUARTER", "YEAR", + "SECOND_OF_MINUTE", + "SECOND_OF_DAY", "MINUTE_OF_HOUR", + "MINUTE_OF_DAY", "HOUR_OF_DAY", "DAY_OF_WEEK", "DAY_OF_MONTH", diff --git a/packages/gooddata-pandas/src/gooddata_pandas/arrow_convertor.py b/packages/gooddata-pandas/src/gooddata_pandas/arrow_convertor.py index 8b2d8b6fe..93281d877 100644 --- a/packages/gooddata-pandas/src/gooddata_pandas/arrow_convertor.py +++ b/packages/gooddata-pandas/src/gooddata_pandas/arrow_convertor.py @@ -68,7 +68,7 @@ def _get_date_converter_for_label(label_id: str, model_labels: dict): - ``DAY`` / ``MONTH`` / ``YEAR`` → ``DateConverter`` (→ ``pandas.Timestamp`` via external fn) - ``WEEK`` / ``QUARTER`` → ``StringConverter`` (no-op) - - ``MINUTE`` / ``HOUR`` → ``DatetimeConverter`` + - ``SECOND`` / ``MINUTE`` / ``HOUR`` → ``DatetimeConverter`` - No granularity (text attrs) → ``None`` (caller skips conversion) """ info = model_labels.get(label_id, {}) @@ -84,6 +84,7 @@ def convert_label_values(label_id: str, values: list, model_labels: dict) -> lis Mirrors the non-Arrow execution path (``AttributeConverterStore`` in ``_typed_attribute_value``): - ``DAY`` / ``MONTH`` / ``YEAR`` granularity → ``pandas.Timestamp`` + - ``SECOND`` / ``MINUTE`` / ``HOUR`` → ``pandas.Timestamp`` - ``WEEK`` / ``QUARTER`` → ``str`` (unchanged) - No granularity (text attributes) → values returned as the **same object** diff --git a/packages/gooddata-pandas/tests/utils/test_utils.py b/packages/gooddata-pandas/tests/utils/test_utils.py index a532dca6b..3a623ca8f 100644 --- a/packages/gooddata-pandas/tests/utils/test_utils.py +++ b/packages/gooddata-pandas/tests/utils/test_utils.py @@ -59,6 +59,12 @@ def test_typed_attribute_values_batches_dates_to_timestamps(): pandas.Timestamp("2023-01-01"), pandas.Timestamp("2023-03-01"), ] + assert _typed_attribute_values( + _date_catalog_attribute("SECOND"), ["2026-07-31 12:34:56", "2026-12-31 23:59:59"] + ) == [ + pandas.Timestamp("2026-07-31 12:34:56"), + pandas.Timestamp("2026-12-31 23:59:59"), + ] def test_typed_attribute_values_week_and_quarter_stay_strings(): diff --git a/packages/gooddata-pipelines/src/gooddata_pipelines/ldm_extension/input_processor.py b/packages/gooddata-pipelines/src/gooddata_pipelines/ldm_extension/input_processor.py index 04e8c4bc2..b3814e06a 100644 --- a/packages/gooddata-pipelines/src/gooddata_pipelines/ldm_extension/input_processor.py +++ b/packages/gooddata-pipelines/src/gooddata_pipelines/ldm_extension/input_processor.py @@ -62,6 +62,7 @@ class LdmExtensionDataProcessor: """Create GoodData LDM from validated custom datasets and fields.""" DATE_GRANULARITIES: list[str] = [ + "SECOND", "MINUTE", "HOUR", "DAY", @@ -69,7 +70,10 @@ class LdmExtensionDataProcessor: "MONTH", "QUARTER", "YEAR", + "SECOND_OF_MINUTE", + "SECOND_OF_DAY", "MINUTE_OF_HOUR", + "MINUTE_OF_DAY", "HOUR_OF_DAY", "DAY_OF_WEEK", "DAY_OF_MONTH", diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py index 250988897..594723aa9 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py @@ -264,6 +264,7 @@ def __eq__(self, other: object) -> bool: "DAY", "HOUR", "MINUTE", + "SECOND", "QUARTER_OF_YEAR", "MONTH_OF_YEAR", "WEEK_OF_YEAR", @@ -272,6 +273,9 @@ def __eq__(self, other: object) -> bool: "DAY_OF_WEEK", "HOUR_OF_DAY", "MINUTE_OF_HOUR", + "MINUTE_OF_DAY", + "SECOND_OF_DAY", + "SECOND_OF_MINUTE", "FISCAL_MONTH", "FISCAL_QUARTER", "FISCAL_YEAR", diff --git a/packages/gooddata-sdk/src/gooddata_sdk/type_converter.py b/packages/gooddata-sdk/src/gooddata_sdk/type_converter.py index 0e0e70444..17a343deb 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/type_converter.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/type_converter.py @@ -295,7 +295,7 @@ def build_stores() -> None: AttributeConverterStore.register("DATE", IntegerConverter) AttributeConverterStore.register("DATE", StringConverter, ["WEEK", "QUARTER"]) AttributeConverterStore.register("DATE", DateConverter, ["DAY", "MONTH", "YEAR"]) - AttributeConverterStore.register("DATE", DatetimeConverter, ["MINUTE", "HOUR"]) + AttributeConverterStore.register("DATE", DatetimeConverter, ["SECOND", "MINUTE", "HOUR"]) DBTypeConverterStore.register("date", DateConverter) DBTypeConverterStore.register("timestamp", DatetimeConverter) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/visualization.py b/packages/gooddata-sdk/src/gooddata_sdk/visualization.py index 35e5a9c14..0f3adbcb4 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/visualization.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/visualization.py @@ -49,6 +49,7 @@ "GDC.time.date": "DAY", "GDC.time.hour": "HOUR", "GDC.time.minute": "MINUTE", + "GDC.time.second": "SECOND", "GDC.time.quarter_in_year": "QUARTER_OF_YEAR", "GDC.time.month_in_year": "MONTH_OF_YEAR", "GDC.time.week_in_year": "WEEK_OF_YEAR", @@ -57,6 +58,9 @@ "GDC.time.day_in_week": "DAY_OF_WEEK", "GDC.time.hour_in_day": "HOUR_OF_DAY", "GDC.time.minute_in_hour": "MINUTE_OF_HOUR", + "GDC.time.minute_in_day": "MINUTE_OF_DAY", + "GDC.time.second_in_day": "SECOND_OF_DAY", + "GDC.time.second_in_minute": "SECOND_OF_MINUTE", "GDC.time.fiscal_month": "FISCAL_MONTH", "GDC.time.fiscal_quarter": "FISCAL_QUARTER", "GDC.time.fiscal_year": "FISCAL_YEAR", diff --git a/packages/gooddata-sdk/tests/compute_model/test_date_filters.py b/packages/gooddata-sdk/tests/compute_model/test_date_filters.py index 7d69f92a3..8b4350f2b 100644 --- a/packages/gooddata-sdk/tests/compute_model/test_date_filters.py +++ b/packages/gooddata-sdk/tests/compute_model/test_date_filters.py @@ -109,3 +109,40 @@ def test_date_filters_description(scenario, filter, descriptions): def test_all_time_date_filter_is_noop_by_default(): f = AllTimeDateFilter(dataset=ObjId(type="dataset", id="dataset.id")) assert f.is_noop() + + +@pytest.mark.parametrize( + "granularity", + [ + "YEAR", + "QUARTER", + "MONTH", + "WEEK", + "DAY", + "HOUR", + "MINUTE", + "SECOND", + "QUARTER_OF_YEAR", + "MONTH_OF_YEAR", + "WEEK_OF_YEAR", + "DAY_OF_YEAR", + "DAY_OF_MONTH", + "DAY_OF_WEEK", + "HOUR_OF_DAY", + "MINUTE_OF_HOUR", + "MINUTE_OF_DAY", + "SECOND_OF_DAY", + "SECOND_OF_MINUTE", + "FISCAL_MONTH", + "FISCAL_QUARTER", + "FISCAL_YEAR", + ], +) +def test_relative_date_filter_accepts_all_supported_granularities(granularity): + f = RelativeDateFilter( + dataset=ObjId(type="dataset", id="dataset.id"), + granularity=granularity, + from_shift=-30, + to_shift=-1, + ) + assert f.granularity == granularity diff --git a/packages/gooddata-sdk/tests/test_type_converter.py b/packages/gooddata-sdk/tests/test_type_converter.py index f1edc38c0..e118606b9 100644 --- a/packages/gooddata-sdk/tests/test_type_converter.py +++ b/packages/gooddata-sdk/tests/test_type_converter.py @@ -66,6 +66,10 @@ def test_to_type_ok(self): c = conv.DatetimeConverter() assert c.to_type(test_value) == datetime.datetime(2021, 10, 20, 11, 0) + def test_second_granularity_values_convert_to_datetime(self): + c = conv.AttributeConverterStore.find_converter("DATE", "SECOND") + assert c.to_type("2026-07-31 12:34:56") == datetime.datetime(2026, 7, 31, 12, 34, 56) + def test_to_type_wrong_val(self): test_value = "2021-10-20" c = conv.DatetimeConverter()