From 83ce6b5a8f69f92e094ae6eace08ad319d9758a5 Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:47:12 +0000 Subject: [PATCH 1/3] feat(gooddata-sdk): [AUTO] Remove AAC analytics model endpoints and AacAnalyticsModel schemas --- .../src/gooddata_sdk/catalog/workspace/aac.py | 99 +++++++++++++++++-- .../gooddata-sdk/src/gooddata_sdk/config.py | 17 ++++ 2 files changed, 108 insertions(+), 8 deletions(-) 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 0f59bbd06..b3de43926 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py @@ -1,6 +1,7 @@ # (C) 2026 GoodData Corporation from __future__ import annotations +import warnings from pathlib import Path from typing import Any, Literal @@ -24,6 +25,12 @@ from gooddata_sdk.catalog.workspace.declarative_model.workspace.workspace import CatalogDeclarativeWorkspaceModel from gooddata_sdk.utils import read_layout_from_file +_AAC_DEPRECATION_MSG = ( + "AAC analytics model support is deprecated and will be removed in a future version. " + "The AAC analytics model API endpoints have been removed from the GoodData platform. " + "Use the declarative workspace model API directly instead." +) + # AAC types that map to visualization objects (all go through yaml_visualisation_to_declarative) _VISUALIZATION_TYPES = frozenset( { @@ -95,11 +102,15 @@ def aac_dataset_to_declarative( ) -> dict[str, Any]: """Convert an AAC dataset dict to declarative format. + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + Args: aac: The AAC dataset dict (parsed YAML). entities: Optional list of all AAC objects for cross-reference resolution. data_source_id: Optional data source ID for table-based datasets. """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) ent = entities if entities is not None else _build_entities([aac]) args: list[Any] = [ent, aac] if data_source_id is not None: @@ -108,12 +119,22 @@ def aac_dataset_to_declarative( def aac_date_dataset_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: - """Convert an AAC date dataset dict to declarative format.""" + """Convert an AAC date dataset dict to declarative format. + + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) return yaml_date_dataset_to_declarative(aac) def aac_metric_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: - """Convert an AAC metric dict to declarative format.""" + """Convert an AAC metric dict to declarative format. + + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) return yaml_metric_to_declarative(aac) @@ -123,10 +144,14 @@ def aac_visualization_to_declarative( ) -> dict[str, Any]: """Convert an AAC visualization dict to declarative format. + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + Args: aac: The AAC visualization dict (parsed YAML). entities: Optional list of all AAC objects for cross-reference resolution. """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) ent = entities if entities is not None else _build_entities([aac]) return yaml_visualisation_to_declarative(ent, aac) @@ -137,21 +162,35 @@ def aac_dashboard_to_declarative( ) -> dict[str, Any]: """Convert an AAC dashboard dict to declarative format. + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + Args: aac: The AAC dashboard dict (parsed YAML). entities: Optional list of all AAC objects for cross-reference resolution. """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) ent = entities if entities is not None else _build_entities([aac]) return yaml_dashboard_to_declarative(ent, aac) def aac_plugin_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: - """Convert an AAC plugin dict to declarative format.""" + """Convert an AAC plugin dict to declarative format. + + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) return yaml_plugin_to_declarative(aac) def aac_attribute_hierarchy_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: - """Convert an AAC attribute hierarchy dict to declarative format.""" + """Convert an AAC attribute hierarchy dict to declarative format. + + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) return yaml_attribute_hierarchy_to_declarative(aac) @@ -168,23 +207,37 @@ def declarative_dataset_to_aac( ) -> dict[str, Any]: """Convert a declarative dataset dict to AAC format. + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + Args: declarative: The declarative dataset dict. profile: Optional profile dict with host, token, workspace_id, data_source. tables_map: Optional mapping of data source ID to list of table definitions. """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) p = profile if profile is not None else {} t = tables_map if tables_map is not None else {} return declarative_dataset_to_yaml(declarative, p, t) def declarative_date_instance_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: - """Convert a declarative date instance dict to AAC format.""" + """Convert a declarative date instance dict to AAC format. + + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) return declarative_date_instance_to_yaml(declarative) def declarative_metric_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: - """Convert a declarative metric dict to AAC format.""" + """Convert a declarative metric dict to AAC format. + + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) return declarative_metric_to_yaml(declarative) @@ -194,10 +247,14 @@ def declarative_visualization_to_aac( ) -> dict[str, Any]: """Convert a declarative visualization dict to AAC format. + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + Args: declarative: The declarative visualization dict. entities: Optional entities list for cross-reference resolution. """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) ent = entities if entities is not None else [] return declarative_visualisation_to_yaml(ent, declarative) @@ -208,21 +265,35 @@ def declarative_dashboard_to_aac( ) -> dict[str, Any]: """Convert a declarative dashboard dict to AAC format. + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + Args: declarative: The declarative dashboard dict. entities: Optional entities list for cross-reference resolution. """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) 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]: - """Convert a declarative plugin dict to AAC format.""" + """Convert a declarative plugin dict to AAC format. + + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) return declarative_plugin_to_yaml(declarative) def declarative_attribute_hierarchy_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: - """Convert a declarative attribute hierarchy dict to AAC format.""" + """Convert a declarative attribute hierarchy dict to AAC format. + + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) return declarative_attribute_hierarchy_to_yaml(declarative) @@ -234,12 +305,16 @@ def declarative_attribute_hierarchy_to_aac(declarative: dict[str, Any]) -> dict[ def detect_yaml_format(path: Path) -> Literal["aac", "declarative"]: """Detect whether a directory contains AAC or declarative YAMLs. + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + AAC files have a top-level 'type' field (dataset, metric, line_chart, etc.) and live as flat files or in typed subdirectories (datasets/, metrics/, etc.). Declarative files follow a nested folder structure with workspaces//ldm/ and workspaces//analytics_model/. """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) # Check for declarative structure markers if (path / "workspaces").is_dir() or (path / "ldm").is_dir() or (path / "analytics_model").is_dir(): return "declarative" @@ -294,6 +369,9 @@ def load_aac_workspace_from_disk( ) -> CatalogDeclarativeWorkspaceModel: """Load AAC YAML files from source_dir and return a declarative workspace model. + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + Reads all .yaml/.yml files, detects object type from the 'type' field, converts each to declarative format, and assembles into a CatalogDeclarativeWorkspaceModel. @@ -302,6 +380,7 @@ def load_aac_workspace_from_disk( source_dir: Path to directory containing AAC YAML files. data_source_id: Optional data source ID for table-based datasets. """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) # First pass: read all AAC objects and build entities list aac_objects: list[dict[str, Any]] = [] for yaml_file in _collect_aac_yaml_files(source_dir): @@ -387,9 +466,13 @@ def _write_aac_file(source_dir: Path, subdir: str, obj_id: str, content: str) -> def store_aac_workspace_to_disk(model: CatalogDeclarativeWorkspaceModel, source_dir: Path) -> None: """Convert a declarative workspace model to AAC YAML files and write to disk. + .. deprecated:: + AAC analytics model support is deprecated. Use the declarative workspace model API directly. + Creates typed subdirectories (datasets/, metrics/, visualisations/, dashboards/, plugins/, attributeHierarchies/) under source_dir. """ + warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) source_dir.mkdir(parents=True, exist_ok=True) model_dict = model.to_dict(camel_case=True) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/config.py b/packages/gooddata-sdk/src/gooddata_sdk/config.py index e6611fd74..40362923e 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/config.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/config.py @@ -1,5 +1,6 @@ # (C) 2024 GoodData Corporation import os +import warnings from typing import Any, TypeVar from attrs import asdict, define @@ -52,13 +53,29 @@ def to_dict(self, use_env: bool = False) -> dict[str, str]: return {**base, "token": os.environ[env_var]} +_AAC_CONFIG_DEPRECATION_MSG = ( + "AacConfig is deprecated and will be removed in a future version. " + "The AAC analytics model API endpoints have been removed from the GoodData platform." +) + + @define class AacConfig(ConfigBase): + """Configuration for AAC deployments. + + .. deprecated:: + AacConfig is deprecated. The AAC analytics model API endpoints have been removed + from the GoodData platform. + """ + profiles: dict[str, Profile] default_profile: str access: dict[str, str] | None = None source_dir: str | None = None + def __attrs_post_init__(self) -> None: + warnings.warn(_AAC_CONFIG_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) + def ds_credentials(self) -> dict[str, str]: load_dotenv() if self.access is None: From e6c2c968ed1282bada7f402c9d51239736426cfb Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:58:31 +0000 Subject: [PATCH 2/3] fix(gooddata-sdk): address review feedback --- .../gooddata-sdk/src/gooddata_sdk/__init__.py | 19 - .../src/gooddata_sdk/catalog/workspace/aac.py | 508 ------------------ .../gooddata_sdk/catalog/workspace/service.py | 27 - .../src/gooddata_sdk/cli/clone.py | 5 +- .../src/gooddata_sdk/cli/deploy.py | 30 +- .../src/gooddata_sdk/cli/gdc_core.py | 9 +- .../gooddata-sdk/src/gooddata_sdk/config.py | 29 - .../gooddata-sdk/src/gooddata_sdk/utils.py | 42 +- .../gooddata-sdk/tests/catalog/test_aac.py | 377 ------------- 9 files changed, 40 insertions(+), 1006 deletions(-) delete mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py delete mode 100644 packages/gooddata-sdk/tests/catalog/test_aac.py diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index 77397b92d..75175e0af 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -180,25 +180,6 @@ CatalogWorkspacePermissionAssignment, ) from gooddata_sdk.catalog.validate_by_item import CatalogValidateByItem -from gooddata_sdk.catalog.workspace.aac import ( - aac_attribute_hierarchy_to_declarative, - aac_dashboard_to_declarative, - aac_dataset_to_declarative, - aac_date_dataset_to_declarative, - aac_metric_to_declarative, - aac_plugin_to_declarative, - aac_visualization_to_declarative, - declarative_attribute_hierarchy_to_aac, - declarative_dashboard_to_aac, - declarative_dataset_to_aac, - declarative_date_instance_to_aac, - declarative_metric_to_aac, - declarative_plugin_to_aac, - declarative_visualization_to_aac, - detect_yaml_format, - load_aac_workspace_from_disk, - store_aac_workspace_to_disk, -) from gooddata_sdk.catalog.workspace.content_service import CatalogWorkspaceContent, CatalogWorkspaceContentService from gooddata_sdk.catalog.workspace.declarative_model.workspace.analytics_model.analytics_model import ( CatalogDeclarativeAnalytics, diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py deleted file mode 100644 index b3de43926..000000000 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py +++ /dev/null @@ -1,508 +0,0 @@ -# (C) 2026 GoodData Corporation -from __future__ import annotations - -import warnings -from pathlib import Path -from typing import Any, Literal - -from gooddata_code_convertors import ( - declarative_attribute_hierarchy_to_yaml, - declarative_dashboard_to_yaml, - declarative_dataset_to_yaml, - declarative_date_instance_to_yaml, - declarative_metric_to_yaml, - declarative_plugin_to_yaml, - declarative_visualisation_to_yaml, - yaml_attribute_hierarchy_to_declarative, - yaml_dashboard_to_declarative, - yaml_dataset_to_declarative, - yaml_date_dataset_to_declarative, - yaml_metric_to_declarative, - yaml_plugin_to_declarative, - yaml_visualisation_to_declarative, -) - -from gooddata_sdk.catalog.workspace.declarative_model.workspace.workspace import CatalogDeclarativeWorkspaceModel -from gooddata_sdk.utils import read_layout_from_file - -_AAC_DEPRECATION_MSG = ( - "AAC analytics model support is deprecated and will be removed in a future version. " - "The AAC analytics model API endpoints have been removed from the GoodData platform. " - "Use the declarative workspace model API directly instead." -) - -# AAC types that map to visualization objects (all go through yaml_visualisation_to_declarative) -_VISUALIZATION_TYPES = frozenset( - { - "bar_chart", - "column_chart", - "line_chart", - "area_chart", - "pie_chart", - "donut_chart", - "scatter_chart", - "bubble_chart", - "heatmap_chart", - "treemap_chart", - "bullet_chart", - "funnel_chart", - "pyramid_chart", - "waterfall_chart", - "sankey_chart", - "dependency_wheel_chart", - "combo_chart", - "headline_chart", - "geo_chart", - "geo_area_chart", - "repeater_chart", - "table", - } -) - -# All known AAC type values -_ALL_AAC_TYPES = ( - frozenset({"dataset", "date", "metric", "dashboard", "plugin", "attribute_hierarchy"}) | _VISUALIZATION_TYPES -) - -# Types that are "dataset-like" for the entities array -_DATASET_TYPES = frozenset({"dataset", "date"}) - - -# --------------------------------------------------------------------------- -# Entities list builder (needed for cross-reference resolution) -# --------------------------------------------------------------------------- - - -def _build_entities(aac_objects: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Build the entities array expected by convertor functions. - - Some convertor functions (dataset, visualization, dashboard) need access to - all objects for cross-reference resolution (e.g., dataset references). - """ - return [ - { - "id": obj["id"], - "type": obj["type"], - "path": f"{obj['id']}.yaml", - "data": obj, - } - for obj in aac_objects - ] - - -# --------------------------------------------------------------------------- -# Individual object conversions: AAC dict → Declarative dict -# --------------------------------------------------------------------------- - - -def aac_dataset_to_declarative( - aac: dict[str, Any], - entities: list[dict[str, Any]] | None = None, - data_source_id: str | None = None, -) -> dict[str, Any]: - """Convert an AAC dataset dict to declarative format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - - Args: - aac: The AAC dataset dict (parsed YAML). - entities: Optional list of all AAC objects for cross-reference resolution. - data_source_id: Optional data source ID for table-based datasets. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - ent = entities if entities is not None else _build_entities([aac]) - args: list[Any] = [ent, aac] - if data_source_id is not None: - args.append(data_source_id) - return yaml_dataset_to_declarative(*args) - - -def aac_date_dataset_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: - """Convert an AAC date dataset dict to declarative format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - return yaml_date_dataset_to_declarative(aac) - - -def aac_metric_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: - """Convert an AAC metric dict to declarative format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - return yaml_metric_to_declarative(aac) - - -def aac_visualization_to_declarative( - aac: dict[str, Any], - entities: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """Convert an AAC visualization dict to declarative format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - - Args: - aac: The AAC visualization dict (parsed YAML). - entities: Optional list of all AAC objects for cross-reference resolution. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - ent = entities if entities is not None else _build_entities([aac]) - return yaml_visualisation_to_declarative(ent, aac) - - -def aac_dashboard_to_declarative( - aac: dict[str, Any], - entities: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """Convert an AAC dashboard dict to declarative format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - - Args: - aac: The AAC dashboard dict (parsed YAML). - entities: Optional list of all AAC objects for cross-reference resolution. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - ent = entities if entities is not None else _build_entities([aac]) - return yaml_dashboard_to_declarative(ent, aac) - - -def aac_plugin_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: - """Convert an AAC plugin dict to declarative format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - return yaml_plugin_to_declarative(aac) - - -def aac_attribute_hierarchy_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: - """Convert an AAC attribute hierarchy dict to declarative format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - return yaml_attribute_hierarchy_to_declarative(aac) - - -# --------------------------------------------------------------------------- -# Individual object conversions: Declarative dict → AAC dict -# Returns {"json": dict, "content": str} -# --------------------------------------------------------------------------- - - -def declarative_dataset_to_aac( - declarative: dict[str, Any], - profile: dict[str, Any] | None = None, - tables_map: dict[str, list[dict[str, Any]]] | None = None, -) -> dict[str, Any]: - """Convert a declarative dataset dict to AAC format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - - Args: - declarative: The declarative dataset dict. - profile: Optional profile dict with host, token, workspace_id, data_source. - tables_map: Optional mapping of data source ID to list of table definitions. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - p = profile if profile is not None else {} - t = tables_map if tables_map is not None else {} - return declarative_dataset_to_yaml(declarative, p, t) - - -def declarative_date_instance_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: - """Convert a declarative date instance dict to AAC format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - return declarative_date_instance_to_yaml(declarative) - - -def declarative_metric_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: - """Convert a declarative metric dict to AAC format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - return declarative_metric_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. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - - Args: - declarative: The declarative visualization dict. - entities: Optional entities list for cross-reference resolution. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - ent = entities if entities is not None else [] - return declarative_visualisation_to_yaml(ent, 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. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - - Args: - declarative: The declarative dashboard dict. - entities: Optional entities list for cross-reference resolution. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - 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]: - """Convert a declarative plugin dict to AAC format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - return declarative_plugin_to_yaml(declarative) - - -def declarative_attribute_hierarchy_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: - """Convert a declarative attribute hierarchy dict to AAC format. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - return declarative_attribute_hierarchy_to_yaml(declarative) - - -# --------------------------------------------------------------------------- -# Format detection -# --------------------------------------------------------------------------- - - -def detect_yaml_format(path: Path) -> Literal["aac", "declarative"]: - """Detect whether a directory contains AAC or declarative YAMLs. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - - AAC files have a top-level 'type' field (dataset, metric, line_chart, etc.) - and live as flat files or in typed subdirectories (datasets/, metrics/, etc.). - - Declarative files follow a nested folder structure with - workspaces//ldm/ and workspaces//analytics_model/. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - # Check for declarative structure markers - if (path / "workspaces").is_dir() or (path / "ldm").is_dir() or (path / "analytics_model").is_dir(): - return "declarative" - - # Check for AAC markers: typed subdirectories - aac_subdirs = {"datasets", "metrics", "visualisations", "dashboards", "plugins", "attributeHierarchies"} - for subdir in aac_subdirs: - if (path / subdir).is_dir(): - return "aac" - - # Check YAML files for top-level 'type' field - for yaml_file in path.glob("*.yaml"): - content = read_layout_from_file(yaml_file) - if isinstance(content, dict) and content.get("type") in _ALL_AAC_TYPES: - return "aac" - break # Only check the first file - - return "declarative" - - -# --------------------------------------------------------------------------- -# Workspace-level: load AAC from disk → CatalogDeclarativeWorkspaceModel -# --------------------------------------------------------------------------- - - -def _collect_aac_yaml_files(source_dir: Path) -> list[Path]: - """Collect all AAC YAML files from source_dir (flat and typed subdirs).""" - files: list[Path] = [] - - # Flat YAML files in source_dir root - files.extend(sorted(source_dir.glob("*.yaml"))) - files.extend(sorted(source_dir.glob("*.yml"))) - - # Typed subdirectories used by gd CLI - for subdir in sorted(source_dir.iterdir()): - if subdir.is_dir() and subdir.name not in { - "data_sources", - "users", - "user_groups", - "workspaces_data_filters", - "workspaces", - }: - files.extend(sorted(subdir.rglob("*.yaml"))) - files.extend(sorted(subdir.rglob("*.yml"))) - - return files - - -def load_aac_workspace_from_disk( - source_dir: Path, - data_source_id: str | None = None, -) -> CatalogDeclarativeWorkspaceModel: - """Load AAC YAML files from source_dir and return a declarative workspace model. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - - Reads all .yaml/.yml files, detects object type from the 'type' field, - converts each to declarative format, and assembles into a - CatalogDeclarativeWorkspaceModel. - - Args: - source_dir: Path to directory containing AAC YAML files. - data_source_id: Optional data source ID for table-based datasets. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - # First pass: read all AAC objects and build entities list - aac_objects: list[dict[str, Any]] = [] - for yaml_file in _collect_aac_yaml_files(source_dir): - content = read_layout_from_file(yaml_file) - if isinstance(content, dict) and "type" in content: - aac_objects.append(content) - - entities = _build_entities(aac_objects) - - # Second pass: convert each object - datasets: list[dict[str, Any]] = [] - date_instances: list[dict[str, Any]] = [] - metrics: list[dict[str, Any]] = [] - visualization_objects: list[dict[str, Any]] = [] - analytical_dashboards: list[dict[str, Any]] = [] - dashboard_plugins: list[dict[str, Any]] = [] - attribute_hierarchies: list[dict[str, Any]] = [] - - for aac in aac_objects: - aac_type = aac.get("type") - - match aac_type: - case "dataset": - datasets.append(aac_dataset_to_declarative(aac, entities, data_source_id)) - case "date": - date_instances.append(aac_date_dataset_to_declarative(aac)) - case "metric": - metrics.append(aac_metric_to_declarative(aac)) - case "dashboard": - result = aac_dashboard_to_declarative(aac, entities) - # Dashboard convertor may return dashboard + filterContext - if isinstance(result, dict) and "dashboard" in result: - analytical_dashboards.append(result["dashboard"]) - else: - analytical_dashboards.append(result) - case "plugin": - dashboard_plugins.append(aac_plugin_to_declarative(aac)) - case "attribute_hierarchy": - attribute_hierarchies.append(aac_attribute_hierarchy_to_declarative(aac)) - case vis_type if vis_type in _VISUALIZATION_TYPES: - visualization_objects.append(aac_visualization_to_declarative(aac, entities)) - - workspace_model_dict: dict[str, Any] = {} - - ldm: dict[str, Any] = {} - if datasets: - ldm["datasets"] = datasets - if date_instances: - ldm["dateInstances"] = date_instances - if ldm: - workspace_model_dict["ldm"] = ldm - - analytics: dict[str, Any] = {} - if metrics: - analytics["metrics"] = metrics - if visualization_objects: - analytics["visualizationObjects"] = visualization_objects - if analytical_dashboards: - analytics["analyticalDashboards"] = analytical_dashboards - if dashboard_plugins: - analytics["dashboardPlugins"] = dashboard_plugins - if attribute_hierarchies: - analytics["attributeHierarchyObjects"] = attribute_hierarchies - if analytics: - workspace_model_dict["analytics"] = analytics - - return CatalogDeclarativeWorkspaceModel.from_dict(workspace_model_dict) - - -# --------------------------------------------------------------------------- -# Workspace-level: CatalogDeclarativeWorkspaceModel → AAC files on disk -# --------------------------------------------------------------------------- - - -def _write_aac_file(source_dir: Path, subdir: str, obj_id: str, content: str) -> None: - """Write a single AAC YAML file to disk.""" - target_dir = source_dir / subdir - target_dir.mkdir(parents=True, exist_ok=True) - target_file = target_dir / f"{obj_id}.yaml" - target_file.write_text(content, encoding="utf-8") - - -def store_aac_workspace_to_disk(model: CatalogDeclarativeWorkspaceModel, source_dir: Path) -> None: - """Convert a declarative workspace model to AAC YAML files and write to disk. - - .. deprecated:: - AAC analytics model support is deprecated. Use the declarative workspace model API directly. - - Creates typed subdirectories (datasets/, metrics/, visualisations/, dashboards/, - plugins/, attributeHierarchies/) under source_dir. - """ - warnings.warn(_AAC_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - source_dir.mkdir(parents=True, exist_ok=True) - model_dict = model.to_dict(camel_case=True) - - ldm = model_dict.get("ldm", {}) - analytics = model_dict.get("analytics", {}) - - for dataset in ldm.get("datasets", []): - result = declarative_dataset_to_aac(dataset) - _write_aac_file(source_dir, "datasets", dataset["id"], result["content"]) - - for date_instance in ldm.get("dateInstances", []): - result = declarative_date_instance_to_aac(date_instance) - _write_aac_file(source_dir, "datasets", date_instance["id"], result["content"]) - - for metric in analytics.get("metrics", []): - result = declarative_metric_to_aac(metric) - _write_aac_file(source_dir, "metrics", metric["id"], result["content"]) - - for vis in analytics.get("visualizationObjects", []): - result = declarative_visualization_to_aac(vis) - _write_aac_file(source_dir, "visualisations", vis["id"], result["content"]) - - for dashboard in analytics.get("analyticalDashboards", []): - result = declarative_dashboard_to_aac(dashboard) - _write_aac_file(source_dir, "dashboards", dashboard["id"], result["content"]) - - for plugin in analytics.get("dashboardPlugins", []): - result = declarative_plugin_to_aac(plugin) - _write_aac_file(source_dir, "plugins", plugin["id"], result["content"]) - - for hierarchy in analytics.get("attributeHierarchyObjects", []): - result = declarative_attribute_hierarchy_to_aac(hierarchy) - _write_aac_file(source_dir, "attributeHierarchies", hierarchy["id"], result["content"]) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/service.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/service.py index 606331e07..369d15fc7 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/service.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/service.py @@ -20,7 +20,6 @@ from gooddata_sdk import CatalogDeclarativeAutomation from gooddata_sdk.catalog.catalog_service_base import CatalogServiceBase from gooddata_sdk.catalog.permission.service import CatalogPermissionService -from gooddata_sdk.catalog.workspace.aac import load_aac_workspace_from_disk, store_aac_workspace_to_disk from gooddata_sdk.catalog.workspace.declarative_model.workspace.workspace import ( CatalogDeclarativeFilterView, CatalogDeclarativeUserDataFilters, @@ -418,32 +417,6 @@ def load_and_put_declarative_workspace(self, workspace_id: str, layout_root_path ) self.put_declarative_workspace(workspace_id=workspace_id, workspace=declarative_workspace) - # AAC (Analytics-as-Code) methods - - def load_and_put_aac_workspace(self, workspace_id: str, source_dir: Path) -> None: - """Load AAC YAML files from source_dir, convert to declarative, and deploy to workspace. - - Args: - workspace_id (str): - Workspace identification string e.g. "demo" - source_dir (Path): - Path to the directory containing AAC YAML files. - """ - workspace_model = load_aac_workspace_from_disk(source_dir) - self.put_declarative_workspace(workspace_id=workspace_id, workspace=workspace_model) - - def get_and_store_aac_workspace(self, workspace_id: str, source_dir: Path) -> None: - """Get declarative workspace from server, convert to AAC YAML files, and write to disk. - - Args: - workspace_id (str): - Workspace identification string e.g. "demo" - source_dir (Path): - Path to the directory where AAC YAML files will be written. - """ - workspace_model = self.get_declarative_workspace(workspace_id=workspace_id) - store_aac_workspace_to_disk(workspace_model, source_dir) - def clone_workspace( self, source_workspace_id: str, diff --git a/packages/gooddata-sdk/src/gooddata_sdk/cli/clone.py b/packages/gooddata-sdk/src/gooddata_sdk/cli/clone.py index 2234e7203..8ca61ac94 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/cli/clone.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/cli/clone.py @@ -4,7 +4,6 @@ from pathlib import Path from gooddata_sdk import GoodDataSdk -from gooddata_sdk.catalog.workspace.aac import store_aac_workspace_to_disk from gooddata_sdk.cli.constants import ( CONFIG_FILE, DATA_SOURCES, @@ -21,9 +20,7 @@ def _clone_workspaces(sdk: GoodDataSdk, path: Path, source_dir: str) -> None: config_path = path / CONFIG_FILE workspace_id = _get_workspace_id(config_path) - source_path = path / source_dir - workspace_model = sdk.catalog_workspace.get_declarative_workspace(workspace_id) - store_aac_workspace_to_disk(workspace_model, source_path) + sdk.catalog_workspace.store_declarative_workspace(workspace_id=workspace_id, layout_root_path=path) @measure_clone(step="data sources") diff --git a/packages/gooddata-sdk/src/gooddata_sdk/cli/deploy.py b/packages/gooddata-sdk/src/gooddata_sdk/cli/deploy.py index 1b14c5db7..feb9828fe 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/cli/deploy.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/cli/deploy.py @@ -9,7 +9,6 @@ CatalogDeclarativeWorkspaceDataFilters, GoodDataSdk, ) -from gooddata_sdk.catalog.workspace.aac import load_aac_workspace_from_disk from gooddata_sdk.cli.constants import ( CONFIG_FILE, DATA_SOURCES, @@ -19,7 +18,6 @@ WORKSPACES_DATA_FILTERS, ) from gooddata_sdk.cli.utils import measure_deploy -from gooddata_sdk.config import AacConfig from gooddata_sdk.utils import read_layout_from_file @@ -28,30 +26,26 @@ def _get_workspace_id(config_path: Path, profile: str = "default") -> str: content = read_layout_from_file(config_path) if not isinstance(content, dict): raise ValueError(f"Invalid config file: {config_path}") - config = AacConfig.from_dict(content) - selected_profile = config.default_profile if profile == "default" else profile - if selected_profile not in config.profiles: - raise ValueError(f"Profile '{selected_profile}' not found in config") - p = config.profiles[selected_profile] - if p.workspace_id is None: + profiles = content.get("profiles", {}) + default_profile_name = content.get("default_profile", profile) + selected = default_profile_name if profile == "default" else profile + if selected not in profiles: + raise ValueError(f"Profile '{selected}' not found in config") + workspace_id = profiles[selected].get("workspace_id") + if workspace_id is None: raise ValueError( - f"Profile '{selected_profile}' does not have 'workspace_id' set. Required for deploying workspace objects." + f"Profile '{selected}' does not have 'workspace_id' set. Required for deploying workspace objects." ) - return p.workspace_id + return workspace_id @measure_deploy(step=WORKSPACES) def _deploy_workspaces(sdk: GoodDataSdk, path: Path, source_dir: str) -> None: - source_path = path / source_dir config_path = path / CONFIG_FILE workspace_id = _get_workspace_id(config_path) - workspace_model = load_aac_workspace_from_disk(source_path) - - # Preserve workspace data filters from declarative subdirs - wdf = CatalogDeclarativeWorkspaceDataFilters.load_from_disk(source_path) - if wdf.workspace_data_filters: - workspace_model.remove_wdf_refs() - + workspace_model = sdk.catalog_workspace.load_declarative_workspace( + workspace_id=workspace_id, layout_root_path=path + ) sdk.catalog_workspace.put_declarative_workspace(workspace_id=workspace_id, workspace=workspace_model) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/cli/gdc_core.py b/packages/gooddata-sdk/src/gooddata_sdk/cli/gdc_core.py index ebb8982ad..8878c51f9 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/cli/gdc_core.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/cli/gdc_core.py @@ -7,7 +7,6 @@ from gooddata_sdk.cli.constants import CONFIG_FILE, DEFAULT_SOURCE_DIR from gooddata_sdk.cli.deploy import deploy_all, deploy_granular from gooddata_sdk.cli.utils import _SUPPORTED, Bcolors -from gooddata_sdk.config import AacConfig from gooddata_sdk.utils import read_layout_from_file @@ -25,10 +24,10 @@ def _find_config_file() -> Path: def _get_source_dir(config_path: Path) -> str: """Get source_dir from config file, falling back to default.""" content = read_layout_from_file(config_path) - if isinstance(content, dict) and AacConfig.can_structure(content): - config = AacConfig.from_dict(content) - if config.source_dir is not None: - return config.source_dir + if isinstance(content, dict): + source_dir = content.get("source_dir") + if source_dir is not None: + return source_dir return DEFAULT_SOURCE_DIR diff --git a/packages/gooddata-sdk/src/gooddata_sdk/config.py b/packages/gooddata-sdk/src/gooddata_sdk/config.py index 40362923e..30b12008f 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/config.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/config.py @@ -1,6 +1,5 @@ # (C) 2024 GoodData Corporation import os -import warnings from typing import Any, TypeVar from attrs import asdict, define @@ -53,31 +52,3 @@ def to_dict(self, use_env: bool = False) -> dict[str, str]: return {**base, "token": os.environ[env_var]} -_AAC_CONFIG_DEPRECATION_MSG = ( - "AacConfig is deprecated and will be removed in a future version. " - "The AAC analytics model API endpoints have been removed from the GoodData platform." -) - - -@define -class AacConfig(ConfigBase): - """Configuration for AAC deployments. - - .. deprecated:: - AacConfig is deprecated. The AAC analytics model API endpoints have been removed - from the GoodData platform. - """ - - profiles: dict[str, Profile] - default_profile: str - access: dict[str, str] | None = None - source_dir: str | None = None - - def __attrs_post_init__(self) -> None: - warnings.warn(_AAC_CONFIG_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - - def ds_credentials(self) -> dict[str, str]: - load_dotenv() - if self.access is None: - return {} - return {k: os.environ.get(v[1:], v) for k, v in self.access.items()} diff --git a/packages/gooddata-sdk/src/gooddata_sdk/utils.py b/packages/gooddata-sdk/src/gooddata_sdk/utils.py index 0586cea9b..549f9d4aa 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/utils.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/utils.py @@ -16,12 +16,13 @@ import yaml from cattrs import structure from cattrs.errors import ClassValidationError +from dotenv import load_dotenv from gooddata_api_client import ApiAttributeError from gooddata_api_client.model_utils import OpenApiModel from gooddata_sdk.compute.model.attribute import Attribute from gooddata_sdk.compute.model.base import ObjId -from gooddata_sdk.config import AacConfig, Profile +from gooddata_sdk.config import Profile # Use typing collection types to support python < py3.9 IdObjType = Union[str, ObjId, dict[str, dict[str, str]], dict[str, str]] @@ -271,29 +272,27 @@ def _create_profile_legacy(content: dict) -> dict: raise ValueError(msg) -def _create_profile_aac(profile: str, content: dict) -> dict: - aac_config = AacConfig.from_dict(content) - selected_profile = aac_config.default_profile if profile == "default" else profile - if selected_profile not in aac_config.profiles: - raise ValueError(f"Profile file does not contain the specified profile: {profile}") - return aac_config.profiles[selected_profile].to_dict(use_env=True) - - def _get_profile(profile: str, content: dict) -> dict[str, Any]: - is_aac_config = AacConfig.can_structure(content) - if not is_aac_config and profile not in content: - raise ValueError( - "Configuration is invalid. Please check the documentation for the valid configuration: https://www.gooddata.com/docs/python-sdk/latest/getting-started/" - ) - if is_aac_config: - return _create_profile_aac(profile, content) - else: + if "profiles" in content and "default_profile" in content: + # New config format: {profiles: {name: {...}}, default_profile: name} + profiles = content["profiles"] + default_profile = content["default_profile"] + selected = default_profile if profile == "default" else profile + if selected not in profiles: + raise ValueError(f"Profile file does not contain the specified profile: {profile}") + return structure(profiles[selected], Profile).to_dict(use_env=True) + elif profile in content: + # Legacy format: {profile_name: {...}} warn( "Used configuration is deprecated and will be removed in the future. Please use the new configuration: https://www.gooddata.com/docs/python-sdk/latest/getting-started/", DeprecationWarning, stacklevel=2, ) return _create_profile_legacy(content[profile]) + else: + raise ValueError( + "Configuration is invalid. Please check the documentation for the valid configuration: https://www.gooddata.com/docs/python-sdk/latest/getting-started/" + ) def _get_config_content(path: Union[str, Path]) -> Any: @@ -331,8 +330,13 @@ def profile_content(profile: str = "default", profiles_path: Path = PROFILES_FIL def get_ds_credentials(config_file: Union[str, Path]) -> dict[str, str]: content = _get_config_content(config_file) - config = AacConfig.from_dict(content) - return config.ds_credentials() + if not isinstance(content, dict): + return {} + access = content.get("access") + if not access: + return {} + load_dotenv() + return {k: os.environ.get(v[1:], v) for k, v in access.items()} def good_pandas_profile_content( diff --git a/packages/gooddata-sdk/tests/catalog/test_aac.py b/packages/gooddata-sdk/tests/catalog/test_aac.py deleted file mode 100644 index dd5111d65..000000000 --- a/packages/gooddata-sdk/tests/catalog/test_aac.py +++ /dev/null @@ -1,377 +0,0 @@ -# (C) 2026 GoodData Corporation -from __future__ import annotations - -from pathlib import Path - -import yaml -from gooddata_sdk.catalog.workspace.aac import ( - aac_dataset_to_declarative, - aac_date_dataset_to_declarative, - aac_metric_to_declarative, - 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, -) -from gooddata_sdk.config import AacConfig - -_FIXTURES_DIR = Path(__file__).parent / "unit_tests" / "aac_tests" - - -# --------------------------------------------------------------------------- -# Config alignment tests -# --------------------------------------------------------------------------- - - -class TestAacConfig: - def test_parse_vscode_config(self) -> None: - """VSCode plugin's gooddata.yaml should be parseable by AacConfig.""" - content = yaml.safe_load((_FIXTURES_DIR / "gooddata.yaml").read_text()) - assert AacConfig.can_structure(content) - config = AacConfig.from_dict(content) - assert config.default_profile == "default" - assert config.source_dir == "analytics" - profile = config.profiles["default"] - assert profile.host == "https://test.gooddata.com" - assert profile.workspace_id == "test-workspace" - assert profile.data_source == "test-datasource" - - def test_config_without_access(self) -> None: - """Config without 'access' field (VSCode style) should parse fine.""" - content = { - "profiles": {"dev": {"host": "https://h", "token": "t"}}, - "default_profile": "dev", - } - assert AacConfig.can_structure(content) - config = AacConfig.from_dict(content) - assert config.access is None - assert config.ds_credentials() == {} - - def test_config_with_source_dir(self) -> None: - content = { - "profiles": {"dev": {"host": "https://h", "token": "t"}}, - "default_profile": "dev", - "source_dir": "my_analytics", - } - config = AacConfig.from_dict(content) - assert config.source_dir == "my_analytics" - - -# --------------------------------------------------------------------------- -# Individual conversion tests -# --------------------------------------------------------------------------- - - -class TestIndividualConversions: - def test_metric_round_trip(self) -> None: - aac_input = { - "id": "revenue", - "title": "Revenue", - "maql": "SELECT SUM({fact/amount})", - "format": "#,##0", - } - declarative = aac_metric_to_declarative(aac_input) - assert declarative["id"] == "revenue" - assert declarative["title"] == "Revenue" - assert declarative["content"]["maql"] == "SELECT SUM({fact/amount})" - - result = declarative_metric_to_aac(declarative) - assert "json" in result - assert "content" in result - assert result["json"]["id"] == "revenue" - assert result["json"]["maql"] == "SELECT SUM({fact/amount})" - - def test_dataset_to_declarative(self) -> None: - """Test forward conversion: AAC dataset → declarative.""" - aac_input = { - "type": "dataset", - "id": "orders", - "table_path": "orders", - "title": "Orders", - "primary_key": "order_id", - "fields": { - "order_id": { - "type": "attribute", - "source_column": "order_id", - "data_type": "STRING", - "title": "Order ID", - }, - }, - } - declarative = aac_dataset_to_declarative(aac_input) - assert declarative["id"] == "orders" - assert declarative["title"] == "Orders" - - def test_date_dataset_conversion(self) -> None: - aac_input = { - "type": "date", - "id": "order_date", - "title": "Order Date", - } - declarative = aac_date_dataset_to_declarative(aac_input) - assert declarative["id"] == "order_date" - assert declarative["title"] == "Order Date" - - def test_visualization_to_declarative(self) -> None: - """Test forward conversion: AAC visualization → declarative.""" - 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) - assert declarative["id"] == "my_table" - assert declarative["title"] == "My Table" - - def test_metric_from_fixture(self) -> None: - content = yaml.safe_load((_FIXTURES_DIR / "metrics" / "top_products.yaml").read_text()) - declarative = aac_metric_to_declarative(content) - assert declarative["id"] == "top_products" - assert declarative["title"] == "Top Products" - - def test_dataset_from_fixture(self) -> None: - content = yaml.safe_load((_FIXTURES_DIR / "datasets" / "orders.yaml").read_text()) - declarative = aac_dataset_to_declarative(content) - assert declarative["id"] == "orders" - - def test_visualization_from_fixture(self) -> None: - content = yaml.safe_load((_FIXTURES_DIR / "visualisations" / "ratings.yaml").read_text()) - declarative = aac_visualization_to_declarative(content) - assert declarative["id"] == "71bdc379-384a-4eac-9627-364ea847d977" - - def test_metric_declarative_to_aac(self) -> None: - """Test the full round-trip on metrics (both directions work cleanly).""" - aac_input = { - "id": "budget", - "title": "Budget", - "maql": "SELECT SUM({fact/budget})", - "format": "#,##0.00", - "description": "Total budget", - "tags": ["finance"], - } - declarative = aac_metric_to_declarative(aac_input) - result = declarative_metric_to_aac(declarative) - assert result["json"]["id"] == "budget" - assert result["json"]["maql"] == "SELECT SUM({fact/budget})" - assert isinstance(result["content"], str) - assert "budget" in result["content"] - - def test_dataset_declarative_to_aac(self) -> None: - """Test declarative → AAC for datasets using a fixture-converted dataset.""" - content = yaml.safe_load((_FIXTURES_DIR / "datasets" / "orders.yaml").read_text()) - declarative = aac_dataset_to_declarative(content) - result = declarative_dataset_to_aac(declarative) - 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 -# --------------------------------------------------------------------------- - - -class TestFormatDetection: - def test_detect_aac_with_typed_subdirs(self, tmp_path: Path) -> None: - (tmp_path / "datasets").mkdir() - (tmp_path / "datasets" / "orders.yaml").write_text("type: dataset\nid: orders\n") - assert detect_yaml_format(tmp_path) == "aac" - - def test_detect_aac_with_flat_files(self, tmp_path: Path) -> None: - (tmp_path / "orders.yaml").write_text("type: dataset\nid: orders\n") - assert detect_yaml_format(tmp_path) == "aac" - - def test_detect_declarative_with_workspaces_dir(self, tmp_path: Path) -> None: - (tmp_path / "workspaces").mkdir() - assert detect_yaml_format(tmp_path) == "declarative" - - def test_detect_declarative_with_ldm_dir(self, tmp_path: Path) -> None: - (tmp_path / "ldm").mkdir() - assert detect_yaml_format(tmp_path) == "declarative" - - -# --------------------------------------------------------------------------- -# Workspace-level load/store tests -# --------------------------------------------------------------------------- - - -class TestWorkspaceLoadStore: - def test_load_aac_workspace_from_fixtures(self) -> None: - """Load fixtures excluding dashboards (WASM crypto limitation).""" - # Use a temp dir with only datasets, metrics, visualizations - import shutil - import tempfile - - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - for subdir in ("datasets", "metrics", "visualisations"): - src = _FIXTURES_DIR / subdir - if src.exists(): - shutil.copytree(src, tmp_path / subdir) - - model = load_aac_workspace_from_disk(tmp_path) - model_dict = model.to_dict(camel_case=True) - - ldm = model_dict.get("ldm", {}) - analytics = model_dict.get("analytics", {}) - - # 2 datasets + 1 date dataset in fixtures - assert len(ldm.get("datasets", [])) == 2 - assert len(ldm.get("dateInstances", [])) == 1 - - # 1 metric - assert len(analytics.get("metrics", [])) == 1 - - # 2 visualizations - assert len(analytics.get("visualizationObjects", [])) == 2 - - def test_store_and_reload_metrics(self, tmp_path: Path) -> None: - """Store metric AAC files, then reload and compare.""" - # Create a workspace model with just metrics (clean round-trip) - - aac_metrics = [ - {"id": "m1", "title": "Metric 1", "maql": "SELECT 1", "format": "#,##0"}, - {"id": "m2", "title": "Metric 2", "maql": "SELECT 2", "format": "#,##0.00"}, - ] - metrics_declarative = [aac_metric_to_declarative(m) for m in aac_metrics] - - model_dict = {"analytics": {"metrics": metrics_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 - metric_files = list((tmp_path / "metrics").glob("*.yaml")) - assert len(metric_files) == 2 - - # Reload - reloaded = load_aac_workspace_from_disk(tmp_path) - 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 - datasets_dir = tmp_path / "datasets" - datasets_dir.mkdir() - (datasets_dir / "orders.yaml").write_text( - "type: dataset\nid: orders\ntable_path: orders\ntitle: Orders\nprimary_key: order_id\n" - "fields:\n order_id:\n type: attribute\n source_column: order_id\n data_type: STRING\n" - ) - # Create declarative non-workspace dir (should be ignored) - users_dir = tmp_path / "users" - users_dir.mkdir() - (users_dir / "users.yaml").write_text("- id: admin\n authenticationId: admin\n") - - model = load_aac_workspace_from_disk(tmp_path) - model_dict = model.to_dict(camel_case=True) - assert len(model_dict.get("ldm", {}).get("datasets", [])) == 1 From fa7a4f46c0256397b4c25a082b859dd3467cc3d9 Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:18:54 +0000 Subject: [PATCH 3/3] fix(gooddata-sdk): address review feedback --- .../src/gooddata_sdk/catalog/workspace/aac.py | 425 ++++++++++++++++++ .../gooddata-sdk/src/gooddata_sdk/config.py | 12 + 2 files changed, 437 insertions(+) create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py new file mode 100644 index 000000000..0f59bbd06 --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py @@ -0,0 +1,425 @@ +# (C) 2026 GoodData Corporation +from __future__ import annotations + +from pathlib import Path +from typing import Any, Literal + +from gooddata_code_convertors import ( + declarative_attribute_hierarchy_to_yaml, + declarative_dashboard_to_yaml, + declarative_dataset_to_yaml, + declarative_date_instance_to_yaml, + declarative_metric_to_yaml, + declarative_plugin_to_yaml, + declarative_visualisation_to_yaml, + yaml_attribute_hierarchy_to_declarative, + yaml_dashboard_to_declarative, + yaml_dataset_to_declarative, + yaml_date_dataset_to_declarative, + yaml_metric_to_declarative, + yaml_plugin_to_declarative, + yaml_visualisation_to_declarative, +) + +from gooddata_sdk.catalog.workspace.declarative_model.workspace.workspace import CatalogDeclarativeWorkspaceModel +from gooddata_sdk.utils import read_layout_from_file + +# AAC types that map to visualization objects (all go through yaml_visualisation_to_declarative) +_VISUALIZATION_TYPES = frozenset( + { + "bar_chart", + "column_chart", + "line_chart", + "area_chart", + "pie_chart", + "donut_chart", + "scatter_chart", + "bubble_chart", + "heatmap_chart", + "treemap_chart", + "bullet_chart", + "funnel_chart", + "pyramid_chart", + "waterfall_chart", + "sankey_chart", + "dependency_wheel_chart", + "combo_chart", + "headline_chart", + "geo_chart", + "geo_area_chart", + "repeater_chart", + "table", + } +) + +# All known AAC type values +_ALL_AAC_TYPES = ( + frozenset({"dataset", "date", "metric", "dashboard", "plugin", "attribute_hierarchy"}) | _VISUALIZATION_TYPES +) + +# Types that are "dataset-like" for the entities array +_DATASET_TYPES = frozenset({"dataset", "date"}) + + +# --------------------------------------------------------------------------- +# Entities list builder (needed for cross-reference resolution) +# --------------------------------------------------------------------------- + + +def _build_entities(aac_objects: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Build the entities array expected by convertor functions. + + Some convertor functions (dataset, visualization, dashboard) need access to + all objects for cross-reference resolution (e.g., dataset references). + """ + return [ + { + "id": obj["id"], + "type": obj["type"], + "path": f"{obj['id']}.yaml", + "data": obj, + } + for obj in aac_objects + ] + + +# --------------------------------------------------------------------------- +# Individual object conversions: AAC dict → Declarative dict +# --------------------------------------------------------------------------- + + +def aac_dataset_to_declarative( + aac: dict[str, Any], + entities: list[dict[str, Any]] | None = None, + data_source_id: str | None = None, +) -> dict[str, Any]: + """Convert an AAC dataset dict to declarative format. + + Args: + aac: The AAC dataset dict (parsed YAML). + entities: Optional list of all AAC objects for cross-reference resolution. + data_source_id: Optional data source ID for table-based datasets. + """ + ent = entities if entities is not None else _build_entities([aac]) + args: list[Any] = [ent, aac] + if data_source_id is not None: + args.append(data_source_id) + return yaml_dataset_to_declarative(*args) + + +def aac_date_dataset_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: + """Convert an AAC date dataset dict to declarative format.""" + return yaml_date_dataset_to_declarative(aac) + + +def aac_metric_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: + """Convert an AAC metric dict to declarative format.""" + return yaml_metric_to_declarative(aac) + + +def aac_visualization_to_declarative( + aac: dict[str, Any], + entities: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Convert an AAC visualization dict to declarative format. + + Args: + aac: The AAC visualization dict (parsed YAML). + entities: Optional list of all AAC objects for cross-reference resolution. + """ + ent = entities if entities is not None else _build_entities([aac]) + return yaml_visualisation_to_declarative(ent, aac) + + +def aac_dashboard_to_declarative( + aac: dict[str, Any], + entities: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Convert an AAC dashboard dict to declarative format. + + Args: + aac: The AAC dashboard dict (parsed YAML). + entities: Optional list of all AAC objects for cross-reference resolution. + """ + ent = entities if entities is not None else _build_entities([aac]) + return yaml_dashboard_to_declarative(ent, aac) + + +def aac_plugin_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: + """Convert an AAC plugin dict to declarative format.""" + return yaml_plugin_to_declarative(aac) + + +def aac_attribute_hierarchy_to_declarative(aac: dict[str, Any]) -> dict[str, Any]: + """Convert an AAC attribute hierarchy dict to declarative format.""" + return yaml_attribute_hierarchy_to_declarative(aac) + + +# --------------------------------------------------------------------------- +# Individual object conversions: Declarative dict → AAC dict +# Returns {"json": dict, "content": str} +# --------------------------------------------------------------------------- + + +def declarative_dataset_to_aac( + declarative: dict[str, Any], + profile: dict[str, Any] | None = None, + tables_map: dict[str, list[dict[str, Any]]] | None = None, +) -> dict[str, Any]: + """Convert a declarative dataset dict to AAC format. + + Args: + declarative: The declarative dataset dict. + profile: Optional profile dict with host, token, workspace_id, data_source. + tables_map: Optional mapping of data source ID to list of table definitions. + """ + p = profile if profile is not None else {} + t = tables_map if tables_map is not None else {} + return declarative_dataset_to_yaml(declarative, p, t) + + +def declarative_date_instance_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: + """Convert a declarative date instance dict to AAC format.""" + return declarative_date_instance_to_yaml(declarative) + + +def declarative_metric_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: + """Convert a declarative metric dict to AAC format.""" + return declarative_metric_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], + 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]: + """Convert a declarative plugin dict to AAC format.""" + return declarative_plugin_to_yaml(declarative) + + +def declarative_attribute_hierarchy_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: + """Convert a declarative attribute hierarchy dict to AAC format.""" + return declarative_attribute_hierarchy_to_yaml(declarative) + + +# --------------------------------------------------------------------------- +# Format detection +# --------------------------------------------------------------------------- + + +def detect_yaml_format(path: Path) -> Literal["aac", "declarative"]: + """Detect whether a directory contains AAC or declarative YAMLs. + + AAC files have a top-level 'type' field (dataset, metric, line_chart, etc.) + and live as flat files or in typed subdirectories (datasets/, metrics/, etc.). + + Declarative files follow a nested folder structure with + workspaces//ldm/ and workspaces//analytics_model/. + """ + # Check for declarative structure markers + if (path / "workspaces").is_dir() or (path / "ldm").is_dir() or (path / "analytics_model").is_dir(): + return "declarative" + + # Check for AAC markers: typed subdirectories + aac_subdirs = {"datasets", "metrics", "visualisations", "dashboards", "plugins", "attributeHierarchies"} + for subdir in aac_subdirs: + if (path / subdir).is_dir(): + return "aac" + + # Check YAML files for top-level 'type' field + for yaml_file in path.glob("*.yaml"): + content = read_layout_from_file(yaml_file) + if isinstance(content, dict) and content.get("type") in _ALL_AAC_TYPES: + return "aac" + break # Only check the first file + + return "declarative" + + +# --------------------------------------------------------------------------- +# Workspace-level: load AAC from disk → CatalogDeclarativeWorkspaceModel +# --------------------------------------------------------------------------- + + +def _collect_aac_yaml_files(source_dir: Path) -> list[Path]: + """Collect all AAC YAML files from source_dir (flat and typed subdirs).""" + files: list[Path] = [] + + # Flat YAML files in source_dir root + files.extend(sorted(source_dir.glob("*.yaml"))) + files.extend(sorted(source_dir.glob("*.yml"))) + + # Typed subdirectories used by gd CLI + for subdir in sorted(source_dir.iterdir()): + if subdir.is_dir() and subdir.name not in { + "data_sources", + "users", + "user_groups", + "workspaces_data_filters", + "workspaces", + }: + files.extend(sorted(subdir.rglob("*.yaml"))) + files.extend(sorted(subdir.rglob("*.yml"))) + + return files + + +def load_aac_workspace_from_disk( + source_dir: Path, + data_source_id: str | None = None, +) -> CatalogDeclarativeWorkspaceModel: + """Load AAC YAML files from source_dir and return a declarative workspace model. + + Reads all .yaml/.yml files, detects object type from the 'type' field, + converts each to declarative format, and assembles into a + CatalogDeclarativeWorkspaceModel. + + Args: + source_dir: Path to directory containing AAC YAML files. + data_source_id: Optional data source ID for table-based datasets. + """ + # First pass: read all AAC objects and build entities list + aac_objects: list[dict[str, Any]] = [] + for yaml_file in _collect_aac_yaml_files(source_dir): + content = read_layout_from_file(yaml_file) + if isinstance(content, dict) and "type" in content: + aac_objects.append(content) + + entities = _build_entities(aac_objects) + + # Second pass: convert each object + datasets: list[dict[str, Any]] = [] + date_instances: list[dict[str, Any]] = [] + metrics: list[dict[str, Any]] = [] + visualization_objects: list[dict[str, Any]] = [] + analytical_dashboards: list[dict[str, Any]] = [] + dashboard_plugins: list[dict[str, Any]] = [] + attribute_hierarchies: list[dict[str, Any]] = [] + + for aac in aac_objects: + aac_type = aac.get("type") + + match aac_type: + case "dataset": + datasets.append(aac_dataset_to_declarative(aac, entities, data_source_id)) + case "date": + date_instances.append(aac_date_dataset_to_declarative(aac)) + case "metric": + metrics.append(aac_metric_to_declarative(aac)) + case "dashboard": + result = aac_dashboard_to_declarative(aac, entities) + # Dashboard convertor may return dashboard + filterContext + if isinstance(result, dict) and "dashboard" in result: + analytical_dashboards.append(result["dashboard"]) + else: + analytical_dashboards.append(result) + case "plugin": + dashboard_plugins.append(aac_plugin_to_declarative(aac)) + case "attribute_hierarchy": + attribute_hierarchies.append(aac_attribute_hierarchy_to_declarative(aac)) + case vis_type if vis_type in _VISUALIZATION_TYPES: + visualization_objects.append(aac_visualization_to_declarative(aac, entities)) + + workspace_model_dict: dict[str, Any] = {} + + ldm: dict[str, Any] = {} + if datasets: + ldm["datasets"] = datasets + if date_instances: + ldm["dateInstances"] = date_instances + if ldm: + workspace_model_dict["ldm"] = ldm + + analytics: dict[str, Any] = {} + if metrics: + analytics["metrics"] = metrics + if visualization_objects: + analytics["visualizationObjects"] = visualization_objects + if analytical_dashboards: + analytics["analyticalDashboards"] = analytical_dashboards + if dashboard_plugins: + analytics["dashboardPlugins"] = dashboard_plugins + if attribute_hierarchies: + analytics["attributeHierarchyObjects"] = attribute_hierarchies + if analytics: + workspace_model_dict["analytics"] = analytics + + return CatalogDeclarativeWorkspaceModel.from_dict(workspace_model_dict) + + +# --------------------------------------------------------------------------- +# Workspace-level: CatalogDeclarativeWorkspaceModel → AAC files on disk +# --------------------------------------------------------------------------- + + +def _write_aac_file(source_dir: Path, subdir: str, obj_id: str, content: str) -> None: + """Write a single AAC YAML file to disk.""" + target_dir = source_dir / subdir + target_dir.mkdir(parents=True, exist_ok=True) + target_file = target_dir / f"{obj_id}.yaml" + target_file.write_text(content, encoding="utf-8") + + +def store_aac_workspace_to_disk(model: CatalogDeclarativeWorkspaceModel, source_dir: Path) -> None: + """Convert a declarative workspace model to AAC YAML files and write to disk. + + Creates typed subdirectories (datasets/, metrics/, visualisations/, dashboards/, + plugins/, attributeHierarchies/) under source_dir. + """ + source_dir.mkdir(parents=True, exist_ok=True) + model_dict = model.to_dict(camel_case=True) + + ldm = model_dict.get("ldm", {}) + analytics = model_dict.get("analytics", {}) + + for dataset in ldm.get("datasets", []): + result = declarative_dataset_to_aac(dataset) + _write_aac_file(source_dir, "datasets", dataset["id"], result["content"]) + + for date_instance in ldm.get("dateInstances", []): + result = declarative_date_instance_to_aac(date_instance) + _write_aac_file(source_dir, "datasets", date_instance["id"], result["content"]) + + for metric in analytics.get("metrics", []): + result = declarative_metric_to_aac(metric) + _write_aac_file(source_dir, "metrics", metric["id"], result["content"]) + + for vis in analytics.get("visualizationObjects", []): + result = declarative_visualization_to_aac(vis) + _write_aac_file(source_dir, "visualisations", vis["id"], result["content"]) + + for dashboard in analytics.get("analyticalDashboards", []): + result = declarative_dashboard_to_aac(dashboard) + _write_aac_file(source_dir, "dashboards", dashboard["id"], result["content"]) + + for plugin in analytics.get("dashboardPlugins", []): + result = declarative_plugin_to_aac(plugin) + _write_aac_file(source_dir, "plugins", plugin["id"], result["content"]) + + for hierarchy in analytics.get("attributeHierarchyObjects", []): + result = declarative_attribute_hierarchy_to_aac(hierarchy) + _write_aac_file(source_dir, "attributeHierarchies", hierarchy["id"], result["content"]) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/config.py b/packages/gooddata-sdk/src/gooddata_sdk/config.py index 30b12008f..e6611fd74 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/config.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/config.py @@ -52,3 +52,15 @@ def to_dict(self, use_env: bool = False) -> dict[str, str]: return {**base, "token": os.environ[env_var]} +@define +class AacConfig(ConfigBase): + profiles: dict[str, Profile] + default_profile: str + access: dict[str, str] | None = None + source_dir: str | None = None + + def ds_credentials(self) -> dict[str, str]: + load_dotenv() + if self.access is None: + return {} + return {k: os.environ.get(v[1:], v) for k, v in self.access.items()}