diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py index 59eeffc64..0f59bbd06 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py @@ -188,14 +188,32 @@ def declarative_metric_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: return declarative_metric_to_yaml(declarative) -def declarative_visualization_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: - """Convert a declarative visualization dict to AAC format.""" - return declarative_visualisation_to_yaml(declarative) +def declarative_visualization_to_aac( + declarative: dict[str, Any], + entities: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Convert a declarative visualization dict to AAC format. + + Args: + declarative: The declarative visualization dict. + entities: Optional entities list for cross-reference resolution. + """ + ent = entities if entities is not None else [] + return declarative_visualisation_to_yaml(ent, declarative) -def declarative_dashboard_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: - """Convert a declarative dashboard dict to AAC format.""" - return declarative_dashboard_to_yaml(declarative) +def declarative_dashboard_to_aac( + declarative: dict[str, Any], + entities: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Convert a declarative dashboard dict to AAC format. + + Args: + declarative: The declarative dashboard dict. + entities: Optional entities list for cross-reference resolution. + """ + ent = entities if entities is not None else [] + return declarative_dashboard_to_yaml(ent, declarative) def declarative_plugin_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: diff --git a/packages/gooddata-sdk/tests/catalog/test_aac.py b/packages/gooddata-sdk/tests/catalog/test_aac.py index f4ccfe741..64347d3e4 100644 --- a/packages/gooddata-sdk/tests/catalog/test_aac.py +++ b/packages/gooddata-sdk/tests/catalog/test_aac.py @@ -11,6 +11,7 @@ aac_visualization_to_declarative, declarative_dataset_to_aac, declarative_metric_to_aac, + declarative_visualization_to_aac, detect_yaml_format, load_aac_workspace_from_disk, store_aac_workspace_to_disk, @@ -176,6 +177,38 @@ def test_dataset_declarative_to_aac(self) -> None: assert result["json"]["id"] == "orders" assert isinstance(result["content"], str) + def test_visualization_declarative_to_aac(self) -> None: + """Test declarative → AAC for visualizations (round-trip from fixture).""" + content = yaml.safe_load((_FIXTURES_DIR / "visualisations" / "ratings.yaml").read_text()) + declarative = aac_visualization_to_declarative(content) + result = declarative_visualization_to_aac(declarative) + assert result["json"]["id"] == "71bdc379-384a-4eac-9627-364ea847d977" + assert isinstance(result["content"], str) + assert "Ratings" in result["content"] + + def test_visualization_declarative_to_aac_inline(self) -> None: + """Test declarative → AAC for a visualization built from inline data.""" + aac_input = { + "type": "table", + "id": "my_table", + "title": "My Table", + "query": { + "fields": { + "m1": { + "title": "Sum of Amount", + "aggregation": "SUM", + "using": "fact/amount", + }, + }, + }, + "metrics": [{"field": "m1", "format": "#,##0"}], + } + declarative = aac_visualization_to_declarative(aac_input) + result = declarative_visualization_to_aac(declarative) + assert result["json"]["id"] == "my_table" + assert isinstance(result["content"], str) + assert "my_table" in result["content"] + # --------------------------------------------------------------------------- # Format detection tests @@ -263,6 +296,68 @@ def test_store_and_reload_metrics(self, tmp_path: Path) -> None: reloaded_dict = reloaded.to_dict(camel_case=True) assert len(reloaded_dict.get("analytics", {}).get("metrics", [])) == 2 + def test_store_and_reload_visualizations(self, tmp_path: Path) -> None: + """Store visualization AAC files via store_aac_workspace_to_disk, then reload.""" + aac_vis = { + "type": "table", + "id": "test_vis", + "title": "Test Visualization", + "query": { + "fields": { + "m1": { + "title": "Sum of Amount", + "aggregation": "SUM", + "using": "fact/amount", + }, + }, + }, + "metrics": [{"field": "m1", "format": "#,##0"}], + } + vis_declarative = [aac_visualization_to_declarative(aac_vis)] + + model_dict = {"analytics": {"visualizationObjects": vis_declarative}} + from gooddata_sdk.catalog.workspace.declarative_model.workspace.workspace import ( + CatalogDeclarativeWorkspaceModel, + ) + + model = CatalogDeclarativeWorkspaceModel.from_dict(model_dict) + store_aac_workspace_to_disk(model, tmp_path) + + # Verify files were created + vis_files = list((tmp_path / "visualisations").glob("*.yaml")) + assert len(vis_files) == 1 + + # Reload + reloaded = load_aac_workspace_from_disk(tmp_path) + reloaded_dict = reloaded.to_dict(camel_case=True) + assert len(reloaded_dict.get("analytics", {}).get("visualizationObjects", [])) == 1 + + def test_store_and_reload_from_fixtures(self, tmp_path: Path) -> None: + """Load fixtures, store to disk, reload — full round-trip.""" + import shutil + import tempfile + + with tempfile.TemporaryDirectory() as tmp: + fixture_path = Path(tmp) + for subdir in ("datasets", "metrics", "visualisations"): + src = _FIXTURES_DIR / subdir + if src.exists(): + shutil.copytree(src, fixture_path / subdir) + + model = load_aac_workspace_from_disk(fixture_path) + + store_aac_workspace_to_disk(model, tmp_path) + + # Verify visualization files exist + vis_files = list((tmp_path / "visualisations").glob("*.yaml")) + assert len(vis_files) == 2 + + # Reload and verify counts match + reloaded = load_aac_workspace_from_disk(tmp_path) + reloaded_dict = reloaded.to_dict(camel_case=True) + assert len(reloaded_dict.get("analytics", {}).get("visualizationObjects", [])) == 2 + assert len(reloaded_dict.get("analytics", {}).get("metrics", [])) == 1 + def test_load_ignores_non_workspace_dirs(self, tmp_path: Path) -> None: """Ensure load skips declarative non-workspace directories.""" # Create AAC file