diff --git a/.python-version b/.python-version index 455808f8..0c7d5f5f 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12.4 +3.11.4 diff --git a/gooddata-sdk/env.py b/gooddata-sdk/env.py new file mode 100644 index 00000000..42716c03 --- /dev/null +++ b/gooddata-sdk/env.py @@ -0,0 +1,9 @@ +# (C) 2024 GoodData Corporation +# env.py +import os + +# Define environment variables +HOST = os.getenv("GOODDATA_HOST", "https://checklist.staging.stg11.panther.intgdc.com") +TOKEN = os.getenv("GOODDATA_TOKEN", "") +DATASOURCE_ID = os.getenv("DATASOURCE_ID", "your datasource") +WORKSPACE_ID = "your workspace" diff --git a/gooddata-sdk/integration_tests/data_response/column_total_returns_by_month.json b/gooddata-sdk/integration_tests/data_response/column_total_returns_by_month.json new file mode 100644 index 00000000..c822323c --- /dev/null +++ b/gooddata-sdk/integration_tests/data_response/column_total_returns_by_month.json @@ -0,0 +1,30 @@ +{ + "id": "total_returns_per_month", + "title": "Total Returns per Month", + "visualizationType": "COLUMN", + "metrics": [ + { + "id": "total_returns", + "type": "metric", + "title": "Total Returns" + } + ], + "dimensionality": [ + { + "id": "return_date.month", + "type": "attribute", + "title": "Return date - Month/Year" + } + ], + "filters": [], + "suggestions": [ + { + "query": "Switch to a line chart to better visualize the trend of total returns over the months.", + "label": "Line Chart for Trends" + }, + { + "query": "Filter the data to show total returns for this year only.", + "label": "This Year's Returns" + } + ] +} diff --git a/gooddata-sdk/integration_tests/data_response/column_total_returns_by_month_filter_current_year.json b/gooddata-sdk/integration_tests/data_response/column_total_returns_by_month_filter_current_year.json new file mode 100644 index 00000000..3ccb0e52 --- /dev/null +++ b/gooddata-sdk/integration_tests/data_response/column_total_returns_by_month_filter_current_year.json @@ -0,0 +1,33 @@ +{ + "id": "total_returns_per_month_current_year", + "title": "Total Returns Per Month - Current Year", + "visualizationType": "COLUMN", + "metrics": [ + { + "id": "total_returns", + "type": "metric", + "title": "Total Returns" + } + ], + "dimensionality": [ + { + "id": "return_date.month", + "type": "attribute", + "title": "Return date - Month/Year" + } + ], + "filters": [ + { + "using": "return_date", + "granularity": "YEAR", + "_from": 0, + "to": 0 + } + ], + "suggestions": [ + { + "query": "Switch to a line chart to better represent the trend of total returns over months.", + "label": "Show Line Chart Trend" + } + ] +} diff --git a/gooddata-sdk/integration_tests/data_response/line_total_customers_by_month_filter_current_year.json b/gooddata-sdk/integration_tests/data_response/line_total_customers_by_month_filter_current_year.json new file mode 100644 index 00000000..0c453810 --- /dev/null +++ b/gooddata-sdk/integration_tests/data_response/line_total_customers_by_month_filter_current_year.json @@ -0,0 +1,33 @@ +{ + "id": "total_customers_per_month_current_year", + "title": "Total Customers per Month - Current Year", + "visualizationType": "LINE", + "metrics": [ + { + "id": "total_customers", + "type": "metric", + "title": "Total Customers" + } + ], + "dimensionality": [ + { + "id": "date.month", + "type": "attribute", + "title": "Date - Month/Year" + } + ], + "filters": [ + { + "using": "date", + "granularity": "YEAR", + "_from": 0, + "to": 0 + } + ], + "suggestions": [ + { + "query": "Consider switching to a COLUMN chart for clearer comparisons of Total Customers per month.", + "label": "Switch to COLUMN chart" + } + ] +} diff --git a/gooddata-sdk/integration_tests/data_response/line_total_returns_by_month_filter_current_year.json b/gooddata-sdk/integration_tests/data_response/line_total_returns_by_month_filter_current_year.json new file mode 100644 index 00000000..c3c81f90 --- /dev/null +++ b/gooddata-sdk/integration_tests/data_response/line_total_returns_by_month_filter_current_year.json @@ -0,0 +1,33 @@ +{ + "id": "total_returns_per_month_current_year", + "title": "Total Returns Per Month - Current Year", + "visualizationType": "LINE", + "metrics": [ + { + "id": "total_returns", + "type": "metric", + "title": "Total Returns" + } + ], + "dimensionality": [ + { + "id": "return_date.month", + "type": "attribute", + "title": "Return date - Month/Year" + } + ], + "filters": [ + { + "using": "return_date", + "granularity": "YEAR", + "_from": 0, + "to": 0 + } + ], + "suggestions": [ + { + "query": "Switch to a line chart to better represent the trend of total returns over months.", + "label": "Show Line Chart Trend" + } + ] +} diff --git a/gooddata-sdk/integration_tests/fixtures/ai_questions.json b/gooddata-sdk/integration_tests/fixtures/ai_questions.json new file mode 100644 index 00000000..d0722e2a --- /dev/null +++ b/gooddata-sdk/integration_tests/fixtures/ai_questions.json @@ -0,0 +1,18 @@ +[ + { + "question": "What is total returns per month? show as COLUMN chart", + "expected_objects_file": "column_total_returns_by_month.json" + }, + { + "question": "Add filter by current year, show as COLUMN chart", + "expected_objects_file": "column_total_returns_by_month_filter_current_year.json" + }, + { + "question": "Switch to LINE chart", + "expected_objects_file": "line_total_returns_by_month_filter_current_year.json" + }, + { + "question": "Replace metric Total Customers instead of total returns", + "expected_objects_file": "line_total_customers_by_month_filter_current_year.json" + } +] diff --git a/gooddata-sdk/integration_tests/scripts/aiChat.py b/gooddata-sdk/integration_tests/scripts/aiChat.py new file mode 100644 index 00000000..7f86522b --- /dev/null +++ b/gooddata-sdk/integration_tests/scripts/aiChat.py @@ -0,0 +1,83 @@ +# (C) 2024 GoodData Corporation +import os +import sys + +import gooddata_api_client +import pytest +from gooddata_api_client.api import smart_functions_api +from gooddata_api_client.model.chat_history_request import ChatHistoryRequest +from gooddata_api_client.model.chat_history_result import ChatHistoryResult +from gooddata_api_client.model.chat_request import ChatRequest +from utils import load_json, normalize_metrics + +sys.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from env import WORKSPACE_ID + +EXPECTED_OBJECTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data_response") +QUESTIONS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "fixtures") + +questions_list = load_json(os.path.join(QUESTIONS_DIR, "ai_questions.json")) + + +class gooddataAiChatApp: + def __init__(self, api_client, workspace_id): + self.api_instance = smart_functions_api.SmartFunctionsApi(api_client) + self.workspace_id = workspace_id + + async def ask_question(self, question: str): + return self.api_instance.ai_chat(self.workspace_id, ChatRequest(question=question)) + + async def chat_history(self, interaction_id: int, feedback: str): + return self.api_instance.ai_chat_history( + self.workspace_id, ChatHistoryRequest(chat_history_interaction_id=interaction_id, user_feedback=feedback) + ) + + +def validate_response(actual_response, expected_response): + actual_metrics = normalize_metrics( + actual_response["created_visualizations"]["objects"][0]["metrics"], exclude_keys=["title"] + ) + expected_metrics = normalize_metrics(expected_response["metrics"], exclude_keys=["title"]) + assert actual_metrics == expected_metrics, "Metrics do not match" + assert ( + actual_response["created_visualizations"]["objects"][0]["visualization_type"] + == expected_response["visualizationType"] + ), "Visualization type mismatch" + assert ( + actual_response["created_visualizations"]["objects"][0]["dimensionality"] == expected_response["dimensionality"] + ), "Dimensionality mismatch" + assert ( + actual_response["created_visualizations"]["objects"][0]["filters"] == expected_response["filters"] + ), "Filters mismatch" + + +@pytest.fixture(scope="module") +def app(set_authorization_header): # Using the global fixture for Authorization header + app_instance = gooddataAiChatApp(set_authorization_header, WORKSPACE_ID) + return app_instance + + +@pytest.mark.parametrize( + "question, expected_file", + [(item["question"], item["expected_objects_file"]) for item in questions_list], + ids=[item["question"] for item in questions_list], +) +@pytest.mark.asyncio +async def test_ai_chat(app, question, expected_file): + expected_objects = load_json(os.path.join(EXPECTED_OBJECTS_DIR, expected_file)) + try: + api_response = await app.ask_question(question) + validate_response(api_response.to_dict(), expected_objects) + + interaction_id = api_response.chat_history_interaction_id + user_feedback = await app.chat_history(interaction_id, "POSITIVE") + assert isinstance(user_feedback, ChatHistoryResult), "Invalid response from chat history" + except gooddata_api_client.ApiException as e: + pytest.fail(f"API exception: {e}") + except Exception as e: + pytest.fail(f"Unexpected error: {e}") + + +if __name__ == "__main__": + pytest.main() diff --git a/gooddata-sdk/integration_tests/scripts/ai_chat.py b/gooddata-sdk/integration_tests/scripts/ai_chat.py new file mode 100644 index 00000000..b16a2c78 --- /dev/null +++ b/gooddata-sdk/integration_tests/scripts/ai_chat.py @@ -0,0 +1,36 @@ +# (C) 2024 GoodData Corporation +import os +import sys + +import pytest + +# Add the root directory to sys.path +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from env import HOST, TOKEN, WORKSPACE_ID +from gooddata_sdk import GoodDataSdk + + +@pytest.fixture +def test_config(): + return {"host": HOST, "token": TOKEN, "workspace_id": WORKSPACE_ID} + + +questions = [ + "What is the number of Accounts?", + "What is the total of Amount?", +] + + +@pytest.mark.parametrize("question", questions) +def test_ask_ai(test_config, question): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + workspace_id = test_config["workspace_id"] + chat_ai_res = sdk.compute.ai_chat(workspace_id, question=question) + + print(f"Chat AI response: {chat_ai_res}") + assert chat_ai_res is not None, "Response should not be None" + + +if __name__ == "__main__": + pytest.main() diff --git a/gooddata-sdk/integration_tests/scripts/conftest.py b/gooddata-sdk/integration_tests/scripts/conftest.py new file mode 100644 index 00000000..f81dbac9 --- /dev/null +++ b/gooddata-sdk/integration_tests/scripts/conftest.py @@ -0,0 +1,26 @@ +# (C) 2024 GoodData Corporation + +import os +import sys + +import pytest +from env import HOST, TOKEN +from gooddata_api_client import ApiClient, Configuration + +# Add the root directory to the Python path +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(ROOT_DIR) + + +@pytest.fixture(scope="session", autouse=True) +def set_authorization_header(): + """ + Fixture to set the Authorization header globally for all tests. + """ + configuration = Configuration(host=HOST) + configuration.access_token = TOKEN + api_client = ApiClient(configuration) + api_client.default_headers["Authorization"] = f"Bearer {TOKEN}" + yield api_client + # Cleanup after the tests, if necessary (e.g., closing client) + api_client.close() diff --git a/gooddata-sdk/integration_tests/scripts/create_ref_workspace.py b/gooddata-sdk/integration_tests/scripts/create_ref_workspace.py new file mode 100644 index 00000000..dc07c6e7 --- /dev/null +++ b/gooddata-sdk/integration_tests/scripts/create_ref_workspace.py @@ -0,0 +1,16 @@ +# (C) 2024 GoodData Corporation +from env import DATASOURCE_ID, HOST, TOKEN, WORKSPACE_ID +from workspace_manager import createWorkspace, getDataSource, update_env_file + +if __name__ == "__main__": + test_config = {"host": HOST, "token": TOKEN} + + if WORKSPACE_ID: + print(f"Workspace ID '{WORKSPACE_ID}' already exists. Skipping workspace creation.") + else: + workspace_id = createWorkspace(test_config) + dataSource = getDataSource(DATASOURCE_ID, test_config) + if workspace_id: + update_env_file(workspace_id) + else: + print("Failed to create workspace.") diff --git a/gooddata-sdk/integration_tests/scripts/delete_ref_workspace.py b/gooddata-sdk/integration_tests/scripts/delete_ref_workspace.py new file mode 100644 index 00000000..a4a97b9c --- /dev/null +++ b/gooddata-sdk/integration_tests/scripts/delete_ref_workspace.py @@ -0,0 +1,8 @@ +# (C) 2024 GoodData Corporation +from env import HOST, TOKEN +from workspace_manager import deleteWorkspace + +if __name__ == "__main__": + test_config = {"host": HOST, "token": TOKEN} + + deleteWorkspace(test_config) diff --git a/gooddata-sdk/integration_tests/scripts/utils.py b/gooddata-sdk/integration_tests/scripts/utils.py new file mode 100644 index 00000000..4ea73d44 --- /dev/null +++ b/gooddata-sdk/integration_tests/scripts/utils.py @@ -0,0 +1,30 @@ +# (C) 2024 GoodData Corporation +import json + + +def load_json(file_path): + """Load a JSON file and return its contents.""" + with open(file_path) as file: # Removed the "r" as it's the default mode + return json.load(file) + + +def normalize_metrics(metrics, exclude_keys=None): + """ + Normalize keys in the metrics list to camelCase, excluding specified keys. + + :param metrics: List of dictionaries with metric data. + :param exclude_keys: List of keys to exclude from normalization. + :return: List of normalized metric dictionaries. + """ + if exclude_keys is None: + exclude_keys = [] + + def snake_to_camel(snake_str): + components = snake_str.split("_") + return components[0] + "".join(x.title() for x in components[1:]) + + return [ + {snake_to_camel(key): value for key, value in metric.items() if key not in exclude_keys} + for metric in metrics + if isinstance(metric, dict) + ] diff --git a/gooddata-sdk/integration_tests/scripts/workspace_manager.py b/gooddata-sdk/integration_tests/scripts/workspace_manager.py new file mode 100644 index 00000000..1d6a0555 --- /dev/null +++ b/gooddata-sdk/integration_tests/scripts/workspace_manager.py @@ -0,0 +1,73 @@ +# (C) 2024 GoodData Corporation +import os +import sys +import time +import uuid + +from gooddata_sdk import CatalogWorkspace, GoodDataSdk + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +try: + from env import WORKSPACE_ID +except ImportError: + WORKSPACE_ID = None + + +def createWorkspace(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + + workspace_id = uuid.uuid4().hex + timestamp = int(time.time()) + workspace_name = f"pysdk_test_{timestamp}" + + workspace = CatalogWorkspace(workspace_id, workspace_name) + try: + sdk.catalog_workspace.create_or_update(workspace) + workspace_o = sdk.catalog_workspace.get_workspace(workspace_id) + assert workspace_o == workspace + + print(f"Workspace '{workspace_name}' with ID '{workspace_id}' created successfully.") + return workspace_id + except Exception as e: + print(f"An error occurred while creating the workspace: {e}") + return None + + +def deleteWorkspace(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + try: + workspaces = sdk.catalog_workspace.list_workspaces() + for workspace in workspaces: + if workspace.name.startswith("pysdk_test_"): + sdk.catalog_workspace.delete_workspace(workspace.id) + print(f"Workspace '{workspace.name}' with ID '{workspace.id}' deleted successfully.") + remove_env_file() + except Exception as e: + print(f"An error occurred while deleting workspaces: {e}") + + +def update_env_file(workspace_id): + with open("env.py", "a") as f: + f.write(f'\nWORKSPACE_ID = "{workspace_id}"\n') + + +def remove_env_file(): + try: + with open("env.py") as f: # Default mode is 'r' + lines = f.readlines() + with open("env.py", "w") as f: + for line in lines: + if "WORKSPACE_ID" not in line: + f.write(line) + print("Removed WORKSPACE_ID from env.py") + except Exception as e: + print(f"An error occurred while removing WORKSPACE_ID from env.py: {e}") + + +def getDataSource(data_source_id, test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + data_source = sdk.catalog_data_source.get_data_source(data_source_id) + data_source_schema = data_source.schema + print(f"Data source schema: {data_source_schema}") + return data_source_schema diff --git a/gooddata-sdk/integration_tests/scripts/workspace_manager_test.py b/gooddata-sdk/integration_tests/scripts/workspace_manager_test.py new file mode 100644 index 00000000..1e80753e --- /dev/null +++ b/gooddata-sdk/integration_tests/scripts/workspace_manager_test.py @@ -0,0 +1,27 @@ +# (C) 2024 GoodData Corporation +# import pytest +# from workspace_manager import create_workspace, delete_workspace, get_data_source, update_env_file +# from env import DATASOURCE_ID, HOST, TOKEN +# @pytest.fixture +# def test_config(): +# return { +# 'host': HOST, +# 'token': TOKEN +# } + +# def test_create_workspace(test_config): +# workspace_id = create_workspace(test_config) +# assert workspace_id is not None, "Workspace creation failed" +# update_env_file(workspace_id) + +# # def test_delete_workspace(test_config): +# # delete_workspace(test_config) +# # # Assuming the function prints the deletion message, we can check the output +# # # Here we assume that the function works correctly if no exception is raised + +# def test_get_data_source(test_config): +# schema = get_data_source(DATASOURCE_ID, test_config) +# assert schema is not None, "Failed to get data source schema" + +# if __name__ == "__main__": +# pytest.main()