From 20b5b05facd146c93d65e60e8b69817c33900919 Mon Sep 17 00:00:00 2001 From: jher Date: Mon, 23 Feb 2026 15:50:46 +0100 Subject: [PATCH] feat: exec initiator into FlexConnect JIRA: CQ-1944 risk: low --- .../execution-context/execution-context.json | 27 ++++ .../src/gooddata_flexconnect/__init__.py | 5 + .../function/execution_context.py | 126 ++++++++++++++++++ .../tests/function/conftest.py | 6 + .../test_flex_fun_execution_context.py | 66 +++++++++ .../test_execution_context_schema.py | 76 +++++++++++ 6 files changed, 306 insertions(+) diff --git a/packages/gooddata-flexconnect/json_schemas/execution-context/execution-context.json b/packages/gooddata-flexconnect/json_schemas/execution-context/execution-context.json index b46a66c44..2c3ff172e 100644 --- a/packages/gooddata-flexconnect/json_schemas/execution-context/execution-context.json +++ b/packages/gooddata-flexconnect/json_schemas/execution-context/execution-context.json @@ -52,6 +52,33 @@ "items": { "$ref": "filter.json" } + }, + "executionInitiator": { + "type": "object", + "description": "Information about what triggered this execution.", + "properties": { + "type": { + "type": "string", + "enum": ["display", "adhocExport", "automation", "alert"], + "description": "The type of execution initiator." + }, + "dashboardId": { + "type": "string" + }, + "visualizationId": { + "type": "string" + }, + "widgetId": { + "type": "string" + }, + "exportType": { + "type": "string" + }, + "automationId": { + "type": "string" + } + }, + "required": ["type"] } }, "allOf": [ diff --git a/packages/gooddata-flexconnect/src/gooddata_flexconnect/__init__.py b/packages/gooddata-flexconnect/src/gooddata_flexconnect/__init__.py index 11e4c8a9a..3a9c3ed91 100644 --- a/packages/gooddata-flexconnect/src/gooddata_flexconnect/__init__.py +++ b/packages/gooddata-flexconnect/src/gooddata_flexconnect/__init__.py @@ -14,6 +14,11 @@ ExecutionContextNegativeAttributeFilter, ExecutionContextPositiveAttributeFilter, ExecutionContextRelativeDateFilter, + ExecutionInitiator, + ExecutionInitiatorAdHocExport, + ExecutionInitiatorAlert, + ExecutionInitiatorAutomation, + ExecutionInitiatorDisplay, ExecutionRequest, ExecutionType, LabelElementsExecutionRequest, diff --git a/packages/gooddata-flexconnect/src/gooddata_flexconnect/function/execution_context.py b/packages/gooddata-flexconnect/src/gooddata_flexconnect/function/execution_context.py index ba6d7136e..f988a38d4 100644 --- a/packages/gooddata-flexconnect/src/gooddata_flexconnect/function/execution_context.py +++ b/packages/gooddata-flexconnect/src/gooddata_flexconnect/function/execution_context.py @@ -436,6 +436,126 @@ def from_dict(d: dict) -> "LabelElementsExecutionRequest": ) +@dataclass +class ExecutionInitiatorDisplay: + """ + Information about an execution being run in order to display the data in the UI. + """ + + dashboard_id: Optional[str] + """ + The id of the dashboard the execution was run as a part of. + """ + + visualization_id: Optional[str] + """ + The id of the visualization the execution was run as a part of. + """ + + widget_id: Optional[str] + """ + The id of the widget the execution was run as a part of. + """ + + +@dataclass +class ExecutionInitiatorAdHocExport: + """ + Information about an execution being run in order to export the data by a user in the UI. + """ + + dashboard_id: Optional[str] + """ + The id of the dashboard the execution was run as a part of. + """ + + visualization_id: Optional[str] + """ + The id of the visualization the execution was run as a part of. + """ + + widget_id: Optional[str] + """ + The id of the widget the execution was run as a part of. + """ + + export_type: Optional[str] + """ + The type of the exported file (CSV, RAW_CSV, etc.). + """ + + +@dataclass +class ExecutionInitiatorAutomation: + """ + Information about an execution being run because of an automation. + """ + + automation_id: Optional[str] + """ + The id of the automation initiating this execution. + """ + + +@dataclass +class ExecutionInitiatorAlert: + """ + Information about an execution being run in order to evaluate an alert. + """ + + dashboard_id: Optional[str] + """ + The id of the dashboard the execution was run as a part of. + """ + + visualization_id: Optional[str] + """ + The id of the visualization the execution was run as a part of. + """ + + widget_id: Optional[str] + """ + The id of the widget the execution was run as a part of. + """ + + +ExecutionInitiator: TypeAlias = Union[ + ExecutionInitiatorDisplay, + ExecutionInitiatorAdHocExport, + ExecutionInitiatorAutomation, + ExecutionInitiatorAlert, +] + + +@none_safe +def _dict_to_execution_initiator(d: dict) -> ExecutionInitiator: + initiator_type = d.get("type") + if initiator_type == "display": + return ExecutionInitiatorDisplay( + dashboard_id=d.get("dashboardId"), + visualization_id=d.get("visualizationId"), + widget_id=d.get("widgetId"), + ) + if initiator_type == "adhocExport": + return ExecutionInitiatorAdHocExport( + export_type=d.get("exportType"), + dashboard_id=d.get("dashboardId"), + visualization_id=d.get("visualizationId"), + widget_id=d.get("widgetId"), + ) + if initiator_type == "automation": + return ExecutionInitiatorAutomation( + automation_id=d.get("automationId"), + ) + if initiator_type == "alert": + return ExecutionInitiatorAlert( + dashboard_id=d.get("dashboardId"), + visualization_id=d.get("visualizationId"), + widget_id=d.get("widgetId"), + ) + raise ValueError(f"Unsupported execution initiator type: {initiator_type}") + + def _dict_to_filter(d: dict) -> ExecutionContextFilter: filter_type = d.get("filterType") if filter_type == "positiveAttributeFilter": @@ -542,6 +662,11 @@ class ExecutionContext: Only present if the execution type is "LABEL_ELEMENTS". """ + execution_initiator: Optional[ExecutionInitiator] + """ + Information about what triggered this execution (e.g. display, export, automation, alert). + """ + @staticmethod @none_safe def from_dict(d: dict) -> "ExecutionContext": @@ -563,6 +688,7 @@ def from_dict(d: dict) -> "ExecutionContext": ), attributes=_dict_to_attributes(d.get("attributes", [])), filters=_dict_to_filters(d.get("filters", [])), + execution_initiator=_dict_to_execution_initiator(d.get("executionInitiator")), ) @staticmethod diff --git a/packages/gooddata-flexconnect/tests/function/conftest.py b/packages/gooddata-flexconnect/tests/function/conftest.py index 1e7b19630..cb956874b 100644 --- a/packages/gooddata-flexconnect/tests/function/conftest.py +++ b/packages/gooddata-flexconnect/tests/function/conftest.py @@ -88,6 +88,12 @@ def sample_report_execution_context_dict(): } ], "filters": [{"filterType": "negativeAttributeFilter", "labelIdentifier": "attribute1", "values": ["id1"]}], + "executionInitiator": { + "type": "display", + "dashboardId": "b2f2d436-9831-4fe0-81df-8c59fd33242b", + "visualizationId": "bf21d8ec-742c-48d7-8100-80663b43622b", + "widgetId": "453844a7-4aa8-4456-be23-ac62b9b3b98a", + }, } diff --git a/packages/gooddata-flexconnect/tests/function/test_flex_fun_execution_context.py b/packages/gooddata-flexconnect/tests/function/test_flex_fun_execution_context.py index cc6f91486..3b9bb8f17 100644 --- a/packages/gooddata-flexconnect/tests/function/test_flex_fun_execution_context.py +++ b/packages/gooddata-flexconnect/tests/function/test_flex_fun_execution_context.py @@ -1,9 +1,16 @@ # (C) 2024 GoodData Corporation +import pytest + from gooddata_flexconnect.function.execution_context import ( ExecutionContext, ExecutionContextAttribute, ExecutionContextNegativeAttributeFilter, + ExecutionInitiatorAdHocExport, + ExecutionInitiatorAlert, + ExecutionInitiatorAutomation, + ExecutionInitiatorDisplay, ExecutionType, + _dict_to_execution_initiator, ) from gooddata_sdk import ( Attribute, @@ -35,6 +42,12 @@ def test_report_execution_context_deser(sample_report_execution_context_dict): assert isinstance(deserialized.attributes[0], ExecutionContextAttribute) assert isinstance(deserialized.filters[0], ExecutionContextNegativeAttributeFilter) + assert deserialized.execution_initiator is not None + assert isinstance(deserialized.execution_initiator, ExecutionInitiatorDisplay) + assert deserialized.execution_initiator.dashboard_id == "b2f2d436-9831-4fe0-81df-8c59fd33242b" + assert deserialized.execution_initiator.visualization_id == "bf21d8ec-742c-48d7-8100-80663b43622b" + assert deserialized.execution_initiator.widget_id == "453844a7-4aa8-4456-be23-ac62b9b3b98a" + def test_label_elements_execution_context_deser(sample_label_execution_context_dict): """ @@ -58,3 +71,56 @@ def test_label_elements_execution_context_deser(sample_label_execution_context_d assert isinstance(deserialized.attributes[0], ExecutionContextAttribute) assert isinstance(deserialized.filters[0], ExecutionContextNegativeAttributeFilter) + + assert deserialized.execution_initiator is None + + +@pytest.mark.parametrize( + "initiator_dict, expected_type", + [ + ( + { + "type": "display", + "dashboardId": "d1", + "visualizationId": "v1", + "widgetId": "w1", + }, + ExecutionInitiatorDisplay, + ), + ( + { + "type": "adhocExport", + "dashboardId": "d1", + "visualizationId": "v1", + "widgetId": "w1", + "exportType": "CSV", + }, + ExecutionInitiatorAdHocExport, + ), + ( + { + "type": "automation", + "automationId": "a1", + }, + ExecutionInitiatorAutomation, + ), + ( + { + "type": "alert", + "dashboardId": "d1", + "visualizationId": "v1", + "widgetId": "w1", + }, + ExecutionInitiatorAlert, + ), + ], + ids=["display", "adhocExport", "automation", "alert"], +) +def test_execution_initiator_deser(initiator_dict, expected_type): + result = _dict_to_execution_initiator(initiator_dict) + assert isinstance(result, expected_type) + + +def test_execution_initiator_unknown_type(): + with pytest.raises(ValueError, match="Unsupported execution initiator type"): + _dict_to_execution_initiator({"type": "unknown"}) diff --git a/packages/gooddata-flexconnect/tests/json_schemas/test_execution_context_schema.py b/packages/gooddata-flexconnect/tests/json_schemas/test_execution_context_schema.py index f36a4ccec..df0936325 100644 --- a/packages/gooddata-flexconnect/tests/json_schemas/test_execution_context_schema.py +++ b/packages/gooddata-flexconnect/tests/json_schemas/test_execution_context_schema.py @@ -16,6 +16,69 @@ "filters": [], "reportExecutionRequest": {}, }, + # REPORT with executionInitiator display + { + "executionType": "REPORT", + "organizationId": "org1", + "workspaceId": "ws1", + "userId": "user1", + "attributes": [], + "filters": [], + "reportExecutionRequest": {}, + "executionInitiator": { + "type": "display", + "dashboardId": "d1", + "visualizationId": "v1", + "widgetId": "w1", + }, + }, + # REPORT with executionInitiator adhocExport + { + "executionType": "REPORT", + "organizationId": "org1", + "workspaceId": "ws1", + "userId": "user1", + "attributes": [], + "filters": [], + "reportExecutionRequest": {}, + "executionInitiator": { + "type": "adhocExport", + "dashboardId": "d1", + "visualizationId": "v1", + "widgetId": "w1", + "exportType": "CSV", + }, + }, + # REPORT with executionInitiator automation + { + "executionType": "REPORT", + "organizationId": "org1", + "workspaceId": "ws1", + "userId": "user1", + "attributes": [], + "filters": [], + "reportExecutionRequest": {}, + "executionInitiator": { + "type": "automation", + "automationId": "a1", + }, + }, + # REPORT with executionInitiator alert + { + "executionType": "REPORT", + "organizationId": "org1", + "workspaceId": "ws1", + "userId": "user1", + "attributes": [], + "filters": [], + "reportExecutionRequest": {}, + "executionInitiator": { + "type": "alert", + "dashboardId": "d1", + "visualizationId": "v1", + "widgetId": "w1", + }, + }, # minimal valid schema for LABEL_ELEMENTS execution type { "executionType": "LABEL_ELEMENTS", @@ -110,6 +173,19 @@ def test_valid_execution_context_schema(value, get_validator): "filters": [], "labelElementsExecutionRequest": {"label": "label2"}, }, + # invalid executionInitiator type + { + "executionType": "REPORT", + "organizationId": "org1", + "workspaceId": "ws1", + "userId": "user1", + "attributes": [], + "filters": [], + "reportExecutionRequest": {}, + "executionInitiator": { + "type": "INVALID", + }, + }, ], ) def test_invalid_execution_context_schema(value, get_validator):