diff --git a/.gitignore b/.gitignore index 90e55c849..2f1a00afc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ docs/resources/_gen docs/tmp/ docs/versioned_docs docs/.hugo_build.lock + +integration_tests/**/__pycache__ diff --git a/gooddata-sdk/gooddata_sdk/compute/service.py b/gooddata-sdk/gooddata_sdk/compute/service.py index 3650500df..e26a8bb12 100644 --- a/gooddata-sdk/gooddata_sdk/compute/service.py +++ b/gooddata-sdk/gooddata_sdk/compute/service.py @@ -105,5 +105,21 @@ def ai_chat_history_reset(self, workspace_id: str) -> None: Args: workspace_id: workspace identifier """ - chat_history_request = ChatHistoryRequest(reset=True) + chat_history_request = ChatHistoryRequest( + reset=True, + ) + self._actions_api.ai_chat_history(workspace_id, chat_history_request, _check_return_type=False) + + def ai_chat_history_user_feedback( + self, workspace_id: str, chat_history_interaction_id: int = 0, user_feedback: str = "POSITIVE" + ) -> None: + """ + Reset chat history with AI in GoodData workspace. + + Args: + workspace_id: workspace identifier + """ + chat_history_request = ChatHistoryRequest( + chat_history_interaction_id=chat_history_interaction_id, user_feedback=user_feedback + ) self._actions_api.ai_chat_history(workspace_id, chat_history_request, _check_return_type=False) diff --git a/integration_tests/.env.template b/integration_tests/.env.template new file mode 100644 index 000000000..c6b4f359b --- /dev/null +++ b/integration_tests/.env.template @@ -0,0 +1,6 @@ +# (C) 2024 GoodData Corporation +HOST= +TOKEN= +DATASOURCE_ID= +WORKSPACE_ID= +LLM_TOKEN= diff --git a/integration_tests/__init__.py b/integration_tests/__init__.py new file mode 100644 index 000000000..a6fa20163 --- /dev/null +++ b/integration_tests/__init__.py @@ -0,0 +1 @@ +# (C) 2021 GoodData Corporation diff --git a/integration_tests/conftest.py b/integration_tests/conftest.py new file mode 100644 index 000000000..3d8c3576d --- /dev/null +++ b/integration_tests/conftest.py @@ -0,0 +1,34 @@ +# (C) 2024 GoodData Corporation +# filepath: /Users/tubui/Documents/CODE/gooddata-python-sdk-1/gooddata-sdk/integration_tests/scripts/conftest.py +import os + +import pytest +from dotenv import load_dotenv + +# Load the .env file from the current directory +load_dotenv() + + +@pytest.fixture(scope="session", autouse=True) +def setup_env(): + # Ensure that the environment variables are set + os.environ["HOST"] = os.getenv("HOST", "https://checklist.staging.stg11.panther.intgdc.com") + os.environ["TOKEN"] = os.getenv("TOKEN", "") + os.environ["DATASOURCE_ID"] = os.getenv("DATASOURCE_ID", "") + os.environ["WORKSPACE_ID"] = os.getenv("WORKSPACE_ID", "") + os.environ["DATASOURCE_TYPE"] = os.getenv("DATASOURCE_TYPE", "") + os.environ["DATASOURCE_PASSWORD"] = os.getenv("DATASOURCE_PASSWORD", "") + + # Check if the necessary environment variables are set + if not os.environ["HOST"]: + raise OSError("\nHOST environment variable is not set.") + if not os.environ["TOKEN"]: + raise OSError("\nTOKEN environment variable is not set.") + if not os.environ["DATASOURCE_ID"]: + print("\nWarning: DATA_SOURCE_ID environment variable is not set.") + if not os.environ["WORKSPACE_ID"]: + print("\nWarning: WORKSPACE_ID environment variable is not set.") + if not os.environ["DATASOURCE_TYPE"]: + print("\nWarning: DATASOURCE_TYPE environment variable is not set.") + if not os.environ["DATASOURCE_PASSWORD"]: + print("\nWarning: DATASOURCE_PASSWORD environment variable is not set.") diff --git a/integration_tests/expected/column_total_returns_by_month.json b/integration_tests/expected/column_total_returns_by_month.json new file mode 100644 index 000000000..6fa104ec4 --- /dev/null +++ b/integration_tests/expected/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/integration_tests/expected/headline_count_of_order.json b/integration_tests/expected/headline_count_of_order.json new file mode 100644 index 000000000..11b743152 --- /dev/null +++ b/integration_tests/expected/headline_count_of_order.json @@ -0,0 +1,21 @@ +{ + "id": "number_of_order_ids", + "title": "Number of Order IDs", + "visualizationType": "HEADLINE", + "metrics": [ + { + "id": "order_id", + "type": "attribute", + "title": "Number of Order IDs", + "aggFunction": "COUNT" + } + ], + "dimensionality": [], + "filters": [], + "suggestions": [ + { + "query": "Show the number of orders by year", + "label": "Show by Year" + } + ] +} diff --git a/integration_tests/fixtures/ai_questions.json b/integration_tests/fixtures/ai_questions.json new file mode 100644 index 000000000..540923209 --- /dev/null +++ b/integration_tests/fixtures/ai_questions.json @@ -0,0 +1,10 @@ +[ + { + "question": "What is number of order id, show as HEADLINE chart?", + "expected_objects_file": "headline_count_of_order.json" + }, + { + "question": "What is total returns per month? show as COLUMN chart", + "expected_objects_file": "column_total_returns_by_month.json" + } +] diff --git a/integration_tests/scenarios/__init__.py b/integration_tests/scenarios/__init__.py new file mode 100644 index 000000000..a6fa20163 --- /dev/null +++ b/integration_tests/scenarios/__init__.py @@ -0,0 +1 @@ +# (C) 2021 GoodData Corporation diff --git a/integration_tests/scenarios/aiChat.py b/integration_tests/scenarios/aiChat.py new file mode 100644 index 000000000..1262cf140 --- /dev/null +++ b/integration_tests/scenarios/aiChat.py @@ -0,0 +1,56 @@ +# (C) 2024 GoodData Corporation +import os +import sys +from pprint import pprint + +import pytest +from dotenv import load_dotenv +from gooddata_sdk import GoodDataSdk + +SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPTS_DIR) + +# Load environment variables from the .env file +load_dotenv() + +# Create the test_config dictionary with the loaded environment variables +test_config = {"host": os.getenv("HOST"), "token": os.getenv("TOKEN")} +workspace_id = os.getenv("WORKSPACE_ID") + +questions = ["What is number of order line id ?"] +sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + + +def test_reset_chat_history(): + sdk.compute.ai_chat_history_reset(workspace_id) + + +@pytest.mark.parametrize("question", questions) +def test_ask_ai(question): + chat_ai_res = sdk.compute.ai_chat(workspace_id, question=question) + pprint(chat_ai_res.to_dict()) + assert chat_ai_res["created_visualizations"] is not None, "Created visualizations should not be None" + assert chat_ai_res["routing"] is not None, "Routing should not be None" + + +def test_ai_chat_history(): + chat_ai_res = sdk.compute.ai_chat(workspace_id, question="show me a headline generating net sales and net order") + chat_ai_res.to_dict() + chat_history_interaction_id = chat_ai_res["chat_history_interaction_id"] + pprint(chat_history_interaction_id) + chat_history_res = sdk.compute.ai_chat_history(workspace_id, chat_history_interaction_id) + sdk.compute.ai_chat_history_user_feedback(workspace_id, chat_history_interaction_id, "POSITIVE") + pprint(chat_history_res.to_dict()) + + +def test_get_chat_history(): + chat_history_res = sdk.compute.ai_chat_history(workspace_id) + pprint(chat_history_res.to_dict()) + assert chat_history_res["interactions"] is not None, "Interactions should not be None" + assert ( + chat_history_res["interactions"][0]["question"] == "What is number of order line id ?" + ), "First interaction question should match" + + +if __name__ == "__main__": + pytest.main() diff --git a/integration_tests/scenarios/chatHistory.py b/integration_tests/scenarios/chatHistory.py new file mode 100644 index 000000000..116e9a94f --- /dev/null +++ b/integration_tests/scenarios/chatHistory.py @@ -0,0 +1,97 @@ +# (C) 2024 GoodData Corporation + +import os +from pathlib import Path +from pprint import pprint + +import gooddata_api_client +import pytest +from dotenv import load_dotenv +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_request import ChatRequest + +from integration_tests.scenarios.utils import compare_and_print_diff, load_json, normalize_metrics + +_current_dir = Path(__file__).parent.absolute() +parent_dir = _current_dir.parent +expected_object_dir = parent_dir / "expected" +questions_list_dir = parent_dir / "fixtures" / "ai_questions.json" + +# Load environment variables from the .env file +load_dotenv() + + +@pytest.fixture(scope="module") +def test_config(): + return { + "host": os.getenv("HOST"), + "token": os.getenv("TOKEN"), + "workspace_id": os.getenv("WORKSPACE_ID"), + "llm_token": os.getenv("LLM_TOKEN"), + } + + +@pytest.fixture(scope="module") +def api_client(test_config): + configuration = gooddata_api_client.Configuration(host=test_config["host"]) + api_client = gooddata_api_client.ApiClient(configuration) + api_client.default_headers["Authorization"] = f"Bearer {test_config['token']}" + return api_client + + +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"]) + compare_and_print_diff(actual_metrics, expected_metrics, "Metrics") + actual_visualization_type = actual_response["created_visualizations"]["objects"][0]["visualization_type"] + expected_visualization_type = expected_response["visualizationType"] + compare_and_print_diff(actual_visualization_type, expected_visualization_type, "Visualization type") + actual_dimensionality = actual_response["created_visualizations"]["objects"][0]["dimensionality"] + expected_dimensionality = expected_response["dimensionality"] + compare_and_print_diff(actual_dimensionality, expected_dimensionality, "Dimensionality") + actual_filters = actual_response["created_visualizations"]["objects"][0]["filters"] + expected_filters = expected_response["filters"] + compare_and_print_diff(actual_filters, expected_filters, "Filters") + + +def test_ai_chat_history_reset(api_client, test_config): + api_instance = smart_functions_api.SmartFunctionsApi(api_client) + chat_history_request = ChatHistoryRequest(reset=True) + try: + api_response = api_instance.ai_chat_history(test_config["workspace_id"], chat_history_request) + pprint(api_response) + except gooddata_api_client.ApiException as e: + pytest.fail(f"API exception: {e}") + except Exception as e: + pytest.fail(f"Unexpected error: {e}") + + +questions_list = load_json(questions_list_dir) + + +@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], +) +def test_ai_chat(api_client, test_config, question, expected_file): + expected_objects = load_json(os.path.join(expected_object_dir, expected_file)) + api_instance = smart_functions_api.SmartFunctionsApi(api_client) + try: + api_response = api_instance.ai_chat(test_config["workspace_id"], ChatRequest(question=question)) + print("\napi_response", api_response.created_visualizations.objects[0]) + print("\nexpected_file", expected_objects) + + validate_response(api_response.to_dict(), expected_objects) + + 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(["-s", __file__]) diff --git a/integration_tests/scenarios/llmEndpoint.py b/integration_tests/scenarios/llmEndpoint.py new file mode 100644 index 000000000..32bb1191c --- /dev/null +++ b/integration_tests/scenarios/llmEndpoint.py @@ -0,0 +1,122 @@ +# (C) 2024 GoodData Corporation + +import os +import sys +import time +import uuid +from pprint import pprint + +import gooddata_api_client +import pytest +from dotenv import load_dotenv +from gooddata_api_client.api import llm_endpoints_api, metadata_sync_api, workspaces_entity_apis_api +from gooddata_api_client.model.json_api_llm_endpoint_in import JsonApiLlmEndpointIn +from gooddata_api_client.model.json_api_llm_endpoint_in_attributes import JsonApiLlmEndpointInAttributes +from gooddata_api_client.model.json_api_llm_endpoint_in_document import JsonApiLlmEndpointInDocument +from gooddata_api_client.model.json_api_workspace_in_attributes import JsonApiWorkspaceInAttributes +from gooddata_api_client.model.json_api_workspace_patch import JsonApiWorkspacePatch +from gooddata_api_client.model.json_api_workspace_patch_document import JsonApiWorkspacePatchDocument + +SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPTS_DIR) + +# Load environment variables from the .env file +load_dotenv() + + +@pytest.fixture(scope="module") +def test_config(): + return { + "host": os.getenv("HOST"), + "token": os.getenv("TOKEN"), + "workspace_id": os.getenv("WORKSPACE_ID"), + "llm_token": os.getenv("LLM_TOKEN"), + } + + +@pytest.fixture(scope="module") +def api_client(test_config): + configuration = gooddata_api_client.Configuration(host=test_config["host"]) + api_client = gooddata_api_client.ApiClient(configuration) + api_client.default_headers["Authorization"] = f"Bearer {test_config['token']}" + return api_client + + +def test_create_llm_endpoint(api_client, test_config): + llm_title = f"python_sdk_test_{int(time.time())}" + api_instance = llm_endpoints_api.LLMEndpointsApi(api_client) + json_api_llm_endpoint_in_document = JsonApiLlmEndpointInDocument( + data=JsonApiLlmEndpointIn( + attributes=JsonApiLlmEndpointInAttributes( + llm_model="gpt-4o", + provider="OPENAI", + title=llm_title, + token=test_config["llm_token"], + workspaceIds=[test_config["workspace_id"]], + ), + id=uuid.uuid4().hex, + type="llmEndpoint", + ), + ) + try: + # Post LLM endpoint entities + api_response = api_instance.create_entity_llm_endpoints(json_api_llm_endpoint_in_document) + pprint(api_response) + assert api_response is not None, "API response should not be None" + except gooddata_api_client.ApiException as e: + pytest.fail(f"API exception: {e}") + except Exception as e: + pytest.fail(f"Unexpected error: {e}") + + +def enable_early_access_per_workspace(api_client, test_config, feature_flag_name): + api_instance = workspaces_entity_apis_api.WorkspacesEntityAPIsApi(api_client) + json_api_entities_workspace = api_instance.get_entity_workspaces(test_config["workspace_id"]) + + current_early_access_values = json_api_entities_workspace.data.attributes.early_access_values or [] + updated_early_access_values = list(set(current_early_access_values + [feature_flag_name])) + + json_api_workspace_patch_document = JsonApiWorkspacePatchDocument( + data=JsonApiWorkspacePatch( + attributes=JsonApiWorkspaceInAttributes(early_access_values=updated_early_access_values), + id=test_config["workspace_id"], + type="workspace", + ) + ) + try: + print("Attempting to enable early access feature flag...") + print("workspace_id", test_config["workspace_id"]) + + api_response = api_instance.patch_entity_workspaces( + test_config["workspace_id"], json_api_workspace_patch_document + ) + pprint(api_response) + except gooddata_api_client.ApiException as e: + pytest.fail(f"API exception: {e}") + except Exception as e: + pytest.fail(f"Unexpected error: {e}") + + +def test_enable_gen_ai_per_workspace(api_client, test_config): + enable_early_access_per_workspace(api_client, test_config, "experimental-genai-chat") + + +def test_enable_smart_search_per_workspace(api_client, test_config): + enable_early_access_per_workspace(api_client, test_config, "experimental-semantic-search") + + +def test_metadata_sync(api_client, test_config): + api_instance = metadata_sync_api.MetadataSyncApi(api_client) + try: + print("Attempting to sync metadata...") + print("workspace_id", test_config["workspace_id"]) + + api_instance.metadata_sync(test_config["workspace_id"]) + 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(["-s", __file__]) diff --git a/integration_tests/scenarios/smartSearch.py b/integration_tests/scenarios/smartSearch.py new file mode 100644 index 000000000..7efb46000 --- /dev/null +++ b/integration_tests/scenarios/smartSearch.py @@ -0,0 +1,310 @@ +# (C) 2024 GoodData Corporation + +import os +from pathlib import Path +from pprint import pprint + +import gooddata_api_client +import pytest +from dotenv import load_dotenv +from gooddata_api_client.api import smart_functions_api +from gooddata_api_client.model.search_request import SearchRequest + +_current_dir = Path(__file__).parent.absolute() +parent_dir = _current_dir.parent +expected_object_dir = parent_dir / "expected" +questions_list_dir = parent_dir / "fixtures" / "ai_questions.json" + +# Load environment variables from the .env file +load_dotenv() + + +@pytest.fixture(scope="module") +def test_config(): + return { + "host": os.getenv("HOST"), + "token": os.getenv("TOKEN"), + "workspace_id": os.getenv("WORKSPACE_ID"), + } + + +@pytest.fixture(scope="module") +def api_client(test_config): + configuration = gooddata_api_client.Configuration(host=test_config["host"]) + api_client = gooddata_api_client.ApiClient(configuration) + api_client.default_headers["Authorization"] = f"Bearer {test_config['token']}" + return api_client + + +def test_smart_search_limit_set_to_1(api_client, test_config): + api_instance = smart_functions_api.SmartFunctionsApi(api_client) + search_request = SearchRequest( + deep_search=False, + limit=1, + object_types=["metric", "visualization", "dashboard"], + question="customer state", + relevant_score_threshold=0.3, + title_to_descriptor_ratio=0.7, + ) + try: + api_response = api_instance.ai_search(test_config["workspace_id"], search_request) + pprint(api_response.to_dict()) + assert api_response["results"].__len__() == 1, "Response result should equal to 1" + assert api_response["results"][0].title == "Customers by State", "Response title should be 'Customer by State'" + assert api_response["results"][0].type == "visualization", "Response type should be 'visualization'" + assert api_response["results"][0].score >= 0.7, "Response score should be greater than or equal to 0.7" + except gooddata_api_client.ApiException as e: + pytest.fail(f"API exception: {e}") + except Exception as e: + pytest.fail(f"Unexpected error: {e}") + + +def test_smart_search_limit_set_to_4(api_client, test_config): + api_instance = smart_functions_api.SmartFunctionsApi(api_client) + search_request = SearchRequest( + deep_search=False, + limit=4, + object_types=["metric", "visualization", "dashboard"], + question="Product category", + relevant_score_threshold=0.3, + title_to_descriptor_ratio=0.7, + ) + try: + api_response = api_instance.ai_search(test_config["workspace_id"], search_request) + response_dict = api_response.to_dict() + pprint(response_dict) + + results = response_dict.get("results", []) + assert len(results) == 4, f"Expected 4 results, got {len(results)}" + + expected_results = [ + { + "title": "Product Category Breakdown", + "type": "visualization", + "visualization_url": "local:pyramid", + "score": 0.78567904, + }, + { + "title": "Product Category Rating", + "type": "visualization", + "visualization_url": "local:sankey", + "score": 0.729191, + }, + { + "title": "Net Sales by Product Category", + "type": "visualization", + "visualization_url": "local:donut", + "score": 0.5827423, + }, + { + "title": "Net Sales by Product Category (v2)", + "type": "visualization", + "visualization_url": "local:bar", + "score": 0.52440727, + }, + ] + + for actual, expected in zip(results, expected_results): + assert ( + actual["title"] == expected["title"] + ), f"Expected title '{expected['title']}', got '{actual['title']}'" + assert actual["type"] == expected["type"], f"Expected type '{expected['type']}', got '{actual['type']}'" + assert ( + actual["visualization_url"] == expected["visualization_url"] + ), f"Expected visualization_url '{expected['visualization_url']}', got '{actual['visualization_url']}'" + assert round(actual["score"]) >= round( + expected["score"] + ), f"Expected score >= {round(expected["score"])}, got {round(actual["score"])}" + + expected_relationships = [ + { + "source_object_title": "1. Overview", + "source_object_type": "dashboard", + "target_object_title": "Net Sales by Product Category (v2)", + }, + { + "source_object_title": "4. Products", + "source_object_type": "dashboard", + "target_object_title": "Net Sales by Product Category (v2)", + }, + { + "source_object_title": "4. Products", + "source_object_type": "dashboard", + "target_object_title": "Product Category Breakdown", + }, + { + "source_object_title": "1. Overview", + "source_object_type": "dashboard", + "target_object_title": "Product Category Rating", + }, + ] + + for actual, expected in zip(response_dict.get("relationships", []), expected_relationships): + assert ( + actual["source_object_title"] == expected["source_object_title"] + ), f"Expected source_object_title '{expected['source_object_title']}', got '{actual['source_object_title']}'" + assert ( + actual["source_object_type"] == expected["source_object_type"] + ), f"Expected source_object_type '{expected['source_object_type']}', got '{actual['source_object_type']}'" + assert ( + actual["target_object_title"] == expected["target_object_title"] + ), f"Expected target_object_title '{expected['target_object_title']}', got '{actual['target_object_title']}'" + except gooddata_api_client.ApiException as e: + pytest.fail(f"API exception: {e}") + except Exception as e: + pytest.fail(f"Unexpected error: {e}") + + +def test_smart_search_deep_search(api_client, test_config): + api_instance = smart_functions_api.SmartFunctionsApi(api_client) + search_request = SearchRequest( + deep_search=True, + limit=5, + object_types=["metric", "visualization", "dashboard"], + question="net sales over time", + relevant_score_threshold=0.5, + title_to_descriptor_ratio=0.7, + ) + try: + api_response = api_instance.ai_search(test_config["workspace_id"], search_request) + pprint(api_response.to_dict()) + assert api_response["results"].__len__() == 5, "Response result should equal to 5" + + assert api_response["relationships"].__len__() == 25, "Response result should equal to 25" + except gooddata_api_client.ApiException as e: + pytest.fail(f"API exception: {e}") + except Exception as e: + pytest.fail(f"Unexpected error: {e}") + + +def test_smart_search_attrbute_dataset(api_client, test_config): + api_instance = smart_functions_api.SmartFunctionsApi(api_client) + search_request = SearchRequest( + deep_search=True, + limit=5, + object_types=["attribute", "dataset"], + question="customer email", + relevant_score_threshold=0.5, + title_to_descriptor_ratio=0.7, + ) + try: + api_response = api_instance.ai_search(test_config["workspace_id"], search_request) + pprint(api_response.to_dict()) + results = api_response.to_dict().get("results", []) + assert api_response["results"].__len__() == 5, "Response result should equal to 5" + + assert api_response["relationships"].__len__() == 5, "Response result should equal to 5" + + expected_results = [ + { + "title": "Customer email", + "tags": ["Customer"], + "type": "attribute", + "score": 1.0, + "workspace_id": test_config["workspace_id"], + }, + { + "title": "Customer", + "tags": ["Customer"], + "type": "dataset", + "score": 0.6708147, + "workspace_id": test_config["workspace_id"], + }, + { + "title": "Customer id", + "tags": ["Customer"], + "type": "attribute", + "score": 0.64592105, + "workspace_id": test_config["workspace_id"], + }, + { + "title": "Customer country", + "tags": ["Customer"], + "type": "attribute", + "score": 0.5076896, + "workspace_id": test_config["workspace_id"], + }, + { + "title": "Customer state", + "tags": ["Customer"], + "type": "attribute", + "score": 0.506533, + "workspace_id": test_config["workspace_id"], + }, + ] + + for actual, expected in zip(results, expected_results): + assert ( + actual["title"] == expected["title"] + ), f"Expected title '{expected['title']}', got '{actual['title']}'" + assert actual["tags"] == expected["tags"], f"Expected tags '{expected['tags']}', got '{actual['tags']}'" + assert actual["type"] == expected["type"], f"Expected type '{expected['type']}', got '{actual['type']}'" + assert round(actual["score"]) >= round( + expected["score"] + ), f"Expected score >= {round(expected["score"])}, got {round(actual["score"])}" + assert ( + actual["workspace_id"] == expected["workspace_id"] + ), f"Expected workspace_id '{expected['workspace_id']}', got '{actual['workspace_id']}'" + + except gooddata_api_client.ApiException as e: + pytest.fail(f"API exception: {e}") + except Exception as e: + pytest.fail(f"Unexpected error: {e}") + + +def test_smart_search_label_and_date(api_client, test_config): + api_instance = smart_functions_api.SmartFunctionsApi(api_client) + search_request = SearchRequest( + deep_search=True, + limit=2, + object_types=["label", "date"], + question="Inventory month", + relevant_score_threshold=0.7, + title_to_descriptor_ratio=0.7, + ) + try: + api_response = api_instance.ai_search(test_config["workspace_id"], search_request) + pprint(api_response.to_dict()) + results = api_response.to_dict().get("results", []) + assert api_response["results"].__len__() == 2, "Response result should equal to 2" + + assert api_response["relationships"].__len__() == 0, "There is no relationship" + + expected_results = [ + { + "title": "Inventory month", + "tags": ["Inventory month"], + "type": "date", + "score": 1.0, + "workspace_id": test_config["workspace_id"], + }, + { + "title": "Inventory month - Year", + "tags": ["Inventory month"], + "type": "label", + "score": 0.935153, + "workspace_id": test_config["workspace_id"], + }, + ] + + for actual, expected in zip(results, expected_results): + assert ( + actual["title"] == expected["title"] + ), f"Expected title '{expected['title']}', got '{actual['title']}'" + assert actual["tags"] == expected["tags"], f"Expected tags '{expected['tags']}', got '{actual['tags']}'" + assert actual["type"] == expected["type"], f"Expected type '{expected['type']}', got '{actual['type']}'" + assert round(actual["score"]) >= round( + expected["score"] + ), f"Expected score >= {round(expected["score"])}, got {round(actual["score"])}" + assert ( + actual["workspace_id"] == expected["workspace_id"] + ), f"Expected workspace_id '{expected['workspace_id']}', got '{actual['workspace_id']}'" + + 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(["-s", __file__]) diff --git a/integration_tests/scenarios/utils.py b/integration_tests/scenarios/utils.py new file mode 100644 index 000000000..8d962f424 --- /dev/null +++ b/integration_tests/scenarios/utils.py @@ -0,0 +1,48 @@ +# (C) 2024 GoodData Corporation +import difflib +import json + + +def print_diff(actual, expected, context): + actual_str = json.dumps(actual, indent=4, sort_keys=True) + expected_str = json.dumps(expected, indent=4, sort_keys=True) + diff = difflib.unified_diff( + expected_str.splitlines(), actual_str.splitlines(), fromfile="expected", tofile="actual", lineterm="" + ) + print(f"\n{context} mismatch:") + for line in diff: + print(line) + + +def compare_and_print_diff(actual, expected, context): + if actual != expected: + print_diff(actual, expected, context) + assert actual == expected, f"{context} mismatch" + + +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/integration_tests/scripts/create_ref_dataSource.py b/integration_tests/scripts/create_ref_dataSource.py new file mode 100644 index 000000000..3c7c8f565 --- /dev/null +++ b/integration_tests/scripts/create_ref_dataSource.py @@ -0,0 +1,36 @@ +# (C) 2024 GoodData Corporation +import os +import sys + +import pytest +from dataSource_manager import createDataSource, update_env_file +from dotenv import load_dotenv + +SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPTS_DIR) + + +# Load environment variables from the .env file +load_dotenv() + +# Create the test_config dictionary with the loaded environment variables +test_config = {"host": os.getenv("HOST"), "token": os.getenv("TOKEN")} +dataSourceId = os.getenv("DATASOURCE_ID") + + +def test_create_data_source(): + global dataSourceId + if dataSourceId: + print(f"DataSource ID '{dataSourceId}' already exists. Skipping dataSource creation.") + else: + print("Creating a new dataSource...") + dataSourceId = createDataSource(test_config) + # dataSource = getDataSource(DATASOURCE_ID, test_config) + if dataSourceId: + update_env_file(dataSourceId) + else: + print("Failed to create dataSource.") + + +if __name__ == "__main__": + pytest.main() diff --git a/integration_tests/scripts/create_ref_workspace.py b/integration_tests/scripts/create_ref_workspace.py new file mode 100644 index 000000000..41c9e672f --- /dev/null +++ b/integration_tests/scripts/create_ref_workspace.py @@ -0,0 +1,40 @@ +# (C) 2024 GoodData Corporation +import os +import sys + +import pytest +from dotenv import load_dotenv +from workspace_manager import createWorkspace, update_env_file + +SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPTS_DIR) + + +# Load environment variables from the .env file +load_dotenv() + +# Create the test_config dictionary with the loaded environment variables +test_config = {"host": os.getenv("HOST"), "token": os.getenv("TOKEN")} +workspace_id = os.getenv("WORKSPACE_ID") +dataSource_id = os.getenv("DATASOURCE_ID") + + +def test_create_workspace(): + global workspace_id + if workspace_id: + print(f"Workspace ID '{workspace_id}' already exists. Skipping workspace creation.") + else: + print("Creating a new workspace...") + workspace_id = createWorkspace(test_config) + # dataSource_schema = getDataSource(dataSource_id, test_config) + # print(f"DataSource schema: {dataSource_schema}") + + if workspace_id: + update_env_file(workspace_id) + else: + print("Failed to create workspace.") + # updateWorkSpaceLayout(test_config, workspace_id) + + +if __name__ == "__main__": + pytest.main() diff --git a/integration_tests/scripts/dataSource_manager.py b/integration_tests/scripts/dataSource_manager.py new file mode 100644 index 000000000..47fc257f4 --- /dev/null +++ b/integration_tests/scripts/dataSource_manager.py @@ -0,0 +1,79 @@ +# (C) 2024 GoodData Corporation +import os +import time +import uuid +from datetime import datetime + +from gooddata_sdk import BasicCredentials, CatalogDataSourceSnowflake, GoodDataSdk, SnowflakeAttributes + + +def createDataSource(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + dataSourceId = uuid.uuid4().hex + timestamp = datetime.utcnow().isoformat() + dataSourceName = f"python_sdk_test_ds_{int(time.time())} -- {timestamp}" + data_source = ( + CatalogDataSourceSnowflake( + id=dataSourceId, + name=dataSourceName, + db_specific_attributes=SnowflakeAttributes(account="gooddata", warehouse="TIGER_PERF", db_name="CHECKLIST"), + schema="demo", + credentials=BasicCredentials(username="qa01_test", password=os.getenv("DATASOURCE_PASSWORD")), + ), + ) + print(f"Creating a new dataSource: ${data_source}") + try: + sdk.catalog_data_source.create_or_update_data_source(data_source) + dataSource_o = sdk.catalog_data_source.get_data_source(dataSourceId) + + print(f"DataSource '{dataSource_o}'") + assert dataSource_o == data_source + + print(f"DataSource '{dataSourceName}' with ID '{dataSourceId}' created successfully.") + return dataSourceId + except Exception as e: + print(f"An error occurred while creating the dataSource: {e}") + return None + + +def deleteDataSource(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + try: + print("Attempting to delete dataSource...") + dataSources = sdk.catalog_data_source.list_data_sources() + for dataSource in dataSources: + if dataSource.attributes.name.startswith("python_sdk_test_ds_"): + sdk.catalog_data_source.delete_data_source(dataSource.id) + print(f"dataSource '{dataSource.attributes.name}' with ID '{dataSource.id}' deleted successfully.") + remove_env_file() + except Exception as e: + print(f"An error occurred while deleting dataSource: {e}") + + +def update_env_file(workspace_id): + env_file_path = os.path.join(os.path.dirname(__file__), "..", ".env") + print(f"Updating env.py with WORKSPACE_ID: {workspace_id} into ${env_file_path}") + with open(env_file_path, "a") as f: + f.write(f'\nWORKSPACE_ID = "{workspace_id}"\n') + + +def remove_env_file(): + env_file_path = os.path.join(os.path.dirname(__file__), "..", ".env") + try: + with open(env_file_path) as f: # Default mode is 'r' + lines = f.readlines() + with open(env_file_path, "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/integration_tests/scripts/delete_ref_workspace.py b/integration_tests/scripts/delete_ref_workspace.py new file mode 100644 index 000000000..d55ba5cf5 --- /dev/null +++ b/integration_tests/scripts/delete_ref_workspace.py @@ -0,0 +1,23 @@ +# (C) 2024 GoodData Corporation +import os + +# import sys +import pytest +from dotenv import load_dotenv +from workspace_manager import deleteWorkspace + +# SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) +# sys.path.append(SCRIPTS_DIR) + + +load_dotenv() + +test_config = {"host": os.getenv("HOST"), "token": os.getenv("TOKEN")} + + +def test_delete_workspace(): + deleteWorkspace(test_config) + + +if __name__ == "__main__": + pytest.main(["-s", __file__]) diff --git a/integration_tests/scripts/workspace_manager.py b/integration_tests/scripts/workspace_manager.py new file mode 100644 index 000000000..32e7aa2fe --- /dev/null +++ b/integration_tests/scripts/workspace_manager.py @@ -0,0 +1,92 @@ +# (C) 2024 GoodData Corporation +import json +import os +import time +import uuid +from datetime import datetime +from pathlib import Path + +from gooddata_sdk import CatalogDeclarativeWorkspaces, CatalogWorkspace, GoodDataSdk + +_current_dir = Path(__file__).parent.absolute() +_fixtures_dir = _current_dir / "fixtures" + + +def createWorkspace(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + + workspace_id = uuid.uuid4().hex + timestamp = datetime.utcnow().isoformat() + workspace_name = f"python_sdk_test_{int(time.time())} -- {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: + print("Attempting to delete workspaces...") + workspaces = sdk.catalog_workspace.list_workspaces() + for workspace in workspaces: + if workspace.name.startswith("python_sdk_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): + env_file_path = os.path.join(os.path.dirname(__file__), "..", ".env") + print(f"Updating env.py with WORKSPACE_ID: {workspace_id} into ${env_file_path}") + with open(env_file_path, "a") as f: + f.write(f'\nWORKSPACE_ID = "{workspace_id}"\n') + + +def remove_env_file(): + env_file_path = os.path.join(os.path.dirname(__file__), "..", ".env") + try: + with open(env_file_path) as f: # Default mode is 'r' + lines = f.readlines() + with open(env_file_path, "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 + + +def updateWorkSpaceLayout(test_config, workspaceId): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + # dataSourceSchema= getDataSource(dataSourceId, test_config) + + with open(_fixtures_dir / "demo_load_and_put_declarative_workspaces.yaml") as f: + data = json.load(f) + workspaces_e = CatalogDeclarativeWorkspaces.from_dict(data) + + # try: + sdk.catalog_workspace.put_declarative_workspace( + workspace_id=workspaceId, CatalogDeclarativeWorkspaceModel=workspaces_e + ) + workspace_o = sdk.catalog_workspace.get_declarative_workspaces(workspace_id=workspaceId, exclude=["ACTIVITY_INFO"]) + assert workspaces_e == workspace_o + assert workspaces_e.to_dict(camel_case=True) == workspace_o.to_dict(camel_case=True)