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/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/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