From 86056779adbd7e6414d92a0bbbc934537288ac8d Mon Sep 17 00:00:00 2001 From: Gerda Shank Date: Mon, 11 Sep 2023 13:01:13 -0400 Subject: [PATCH 1/4] Allow setting access in config in addition to properties --- .../unreleased/Features-20230912-153013.yaml | 6 + core/dbt/contracts/graph/model_config.py | 10 +- core/dbt/contracts/graph/nodes.py | 4 +- core/dbt/parser/base.py | 22 +- core/dbt/parser/schemas.py | 4 + schemas/dbt/manifest/v11.json | 201 +++++++++++++++++- .../functional/artifacts/expected_manifest.py | 1 + tests/unit/test_contracts_graph_compiled.py | 7 +- tests/unit/test_contracts_graph_parsed.py | 16 +- tests/unit/test_manifest.py | 6 +- tests/unit/test_parser.py | 10 +- 11 files changed, 264 insertions(+), 23 deletions(-) create mode 100644 .changes/unreleased/Features-20230912-153013.yaml diff --git a/.changes/unreleased/Features-20230912-153013.yaml b/.changes/unreleased/Features-20230912-153013.yaml new file mode 100644 index 00000000000..fc3484d6474 --- /dev/null +++ b/.changes/unreleased/Features-20230912-153013.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Allow setting access in config +time: 2023-09-12T15:30:13.859595-04:00 +custom: + Author: gshank + Issue: "8383" diff --git a/core/dbt/contracts/graph/model_config.py b/core/dbt/contracts/graph/model_config.py index f30ddd8fcc3..b28091e68c1 100644 --- a/core/dbt/contracts/graph/model_config.py +++ b/core/dbt/contracts/graph/model_config.py @@ -14,7 +14,7 @@ from dbt.contracts.util import Replaceable, list_str from dbt.exceptions import DbtInternalError, CompilationError from dbt import hooks -from dbt.node_types import NodeType +from dbt.node_types import NodeType, AccessType from mashumaro.jsonschema.annotations import Pattern @@ -518,6 +518,14 @@ def field_mapping(cls): return {"post_hook": "post-hook", "pre_hook": "pre-hook"} +@dataclass +class ModelConfig(NodeConfig): + access: AccessType = field( + default=AccessType.Protected, + metadata=MergeBehavior.Update.meta(), + ) + + @dataclass class SeedConfig(NodeConfig): materialized: str = "seed" diff --git a/core/dbt/contracts/graph/nodes.py b/core/dbt/contracts/graph/nodes.py index b6e685d985f..35d2ee4d810 100644 --- a/core/dbt/contracts/graph/nodes.py +++ b/core/dbt/contracts/graph/nodes.py @@ -63,6 +63,7 @@ from .model_config import ( NodeConfig, + ModelConfig, SeedConfig, TestConfig, SourceConfig, @@ -569,6 +570,7 @@ class HookNode(CompiledNode): class ModelNode(CompiledNode): resource_type: Literal[NodeType.Model] access: AccessType = AccessType.Protected + config: ModelConfig = field(default_factory=ModelConfig) constraints: List[ModelLevelConstraint] = field(default_factory=list) version: Optional[NodeVersion] = None latest_version: Optional[NodeVersion] = None @@ -605,7 +607,7 @@ def from_args(cls, args: ModelNodeArgs) -> "ModelNode": path="", unrendered_config=unrendered_config, depends_on=DependsOn(nodes=args.depends_on_nodes), - config=NodeConfig(enabled=args.enabled), + config=ModelConfig(enabled=args.enabled), ) @property diff --git a/core/dbt/parser/base.py b/core/dbt/parser/base.py index 17672de1b24..740bce7277b 100644 --- a/core/dbt/parser/base.py +++ b/core/dbt/parser/base.py @@ -18,9 +18,14 @@ from dbt.contracts.graph.manifest import Manifest from dbt.contracts.graph.nodes import Contract, BaseNode, ManifestNode from dbt.contracts.graph.unparsed import Docs, UnparsedNode -from dbt.exceptions import DbtInternalError, ConfigUpdateError, DictParseError +from dbt.exceptions import ( + DbtInternalError, + ConfigUpdateError, + DictParseError, + InvalidAccessTypeError, +) from dbt import hooks -from dbt.node_types import NodeType, ModelLanguage +from dbt.node_types import NodeType, ModelLanguage, AccessType from dbt.parser.search import FileBlock # internally, the parser may store a less-restrictive type that will be @@ -328,6 +333,19 @@ def update_parsed_node_config( if "group" in config_dict and config_dict["group"]: parsed_node.group = config_dict["group"] + # If we have access in the config, copy to node level + if ( + parsed_node.resource_type == NodeType.Model + and "access" in config_dict + and config_dict["access"] + ): + if AccessType.is_valid(config_dict["access"]): + parsed_node.access = AccessType(config_dict["access"]) + else: + raise InvalidAccessTypeError( + unique_id=parsed_node.unique_id, field_value=config_dict["access"] + ) + # If we have docs in the config, merge with the node level, for backwards # compatibility with earlier node-only config. if "docs" in config_dict and config_dict["docs"]: diff --git a/core/dbt/parser/schemas.py b/core/dbt/parser/schemas.py index 24106a85224..eb320686284 100644 --- a/core/dbt/parser/schemas.py +++ b/core/dbt/parser/schemas.py @@ -469,6 +469,7 @@ def get_unparsed_target(self) -> Iterable[NonSourceTarget]: self.normalize_docs_attribute(data, path) self.normalize_group_attribute(data, path) self.normalize_contract_attribute(data, path) + self.normalize_access_attribute(data, path) node = self._target_type().from_dict(data) except (ValidationError, JSONValidationError) as exc: raise YamlParseDictError(path, self.key, data, exc) @@ -503,6 +504,9 @@ def normalize_group_attribute(self, data, path): def normalize_contract_attribute(self, data, path): return self.normalize_attribute(data, path, "contract") + def normalize_access_attribute(self, data, path): + return self.normalize_attribute(data, path, "access") + def patch_node_config(self, node, patch): # Get the ContextConfig that's used in calculating the config # This must match the model resource_type that's being patched diff --git a/schemas/dbt/manifest/v11.json b/schemas/dbt/manifest/v11.json index b133a238369..a9dbd5cf32f 100644 --- a/schemas/dbt/manifest/v11.json +++ b/schemas/dbt/manifest/v11.json @@ -10,7 +10,7 @@ }, "dbt_version": { "type": "string", - "default": "1.7.0b1" + "default": "1.7.0b2" }, "generated_at": { "type": "string" @@ -1463,6 +1463,203 @@ "checksum" ] }, + "ModelConfig": { + "type": "object", + "title": "ModelConfig", + "properties": { + "_extra": { + "type": "object", + "propertyNames": { + "type": "string" + } + }, + "enabled": { + "type": "boolean", + "default": true + }, + "alias": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "database": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "tags": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "meta": { + "type": "object", + "propertyNames": { + "type": "string" + } + }, + "group": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "materialized": { + "type": "string", + "default": "view" + }, + "incremental_strategy": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "persist_docs": { + "type": "object", + "propertyNames": { + "type": "string" + } + }, + "post-hook": { + "type": "array", + "items": { + "$ref": "#/$defs/Hook" + } + }, + "pre-hook": { + "type": "array", + "items": { + "$ref": "#/$defs/Hook" + } + }, + "quoting": { + "type": "object", + "propertyNames": { + "type": "string" + } + }, + "column_types": { + "type": "object", + "propertyNames": { + "type": "string" + } + }, + "full_refresh": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null + }, + "unique_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "default": null + }, + "on_schema_change": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "ignore" + }, + "on_configuration_change": { + "enum": [ + "apply", + "continue", + "fail" + ] + }, + "grants": { + "type": "object", + "propertyNames": { + "type": "string" + } + }, + "packages": { + "type": "array", + "items": { + "type": "string" + } + }, + "docs": { + "$ref": "#/$defs/Docs" + }, + "contract": { + "$ref": "#/$defs/ContractConfig" + }, + "access": { + "enum": [ + "private", + "protected", + "public" + ], + "default": "protected" + } + }, + "additionalProperties": true + }, "ModelLevelConstraint": { "type": "object", "title": "ModelLevelConstraint", @@ -1606,7 +1803,7 @@ "$ref": "#/$defs/FileHash" }, "config": { - "$ref": "#/$defs/NodeConfig" + "$ref": "#/$defs/ModelConfig" }, "_event_status": { "type": "object", diff --git a/tests/functional/artifacts/expected_manifest.py b/tests/functional/artifacts/expected_manifest.py index c6d58e1bc4c..6082ae4b8d4 100644 --- a/tests/functional/artifacts/expected_manifest.py +++ b/tests/functional/artifacts/expected_manifest.py @@ -37,6 +37,7 @@ def get_rendered_model_config(**updates): "incremental_strategy": None, "docs": {"node_color": None, "show": True}, "contract": {"enforced": False}, + "access": "protected", } result.update(updates) return result diff --git a/tests/unit/test_contracts_graph_compiled.py b/tests/unit/test_contracts_graph_compiled.py index 0dc26d3a38b..1b4f0bbd045 100644 --- a/tests/unit/test_contracts_graph_compiled.py +++ b/tests/unit/test_contracts_graph_compiled.py @@ -8,7 +8,7 @@ GenericTestNode, InjectedCTE, ModelNode, - NodeConfig, + ModelConfig, TestConfig, TestMetadata, Contract, @@ -45,7 +45,7 @@ def basic_uncompiled_model(): schema="test_schema", alias="bar", tags=[], - config=NodeConfig(), + config=ModelConfig(), meta={}, compiled=False, extra_ctes=[], @@ -77,7 +77,7 @@ def basic_compiled_model(): schema="test_schema", alias="bar", tags=[], - config=NodeConfig(), + config=ModelConfig(), contract=Contract(), meta={}, compiled=True, @@ -206,6 +206,7 @@ def basic_compiled_dict(): "packages": [], "contract": {"enforced": False}, "docs": {"show": True}, + "access": "protected", }, "docs": {"show": True}, "columns": {}, diff --git a/tests/unit/test_contracts_graph_parsed.py b/tests/unit/test_contracts_graph_parsed.py index 21d5629bc7c..498ec5480e1 100644 --- a/tests/unit/test_contracts_graph_parsed.py +++ b/tests/unit/test_contracts_graph_parsed.py @@ -4,6 +4,7 @@ from dbt.node_types import NodeType, AccessType from dbt.contracts.files import FileHash from dbt.contracts.graph.model_config import ( + ModelConfig, NodeConfig, SeedConfig, TestConfig, @@ -62,7 +63,7 @@ @pytest.fixture def populated_node_config_object(): - result = NodeConfig( + result = ModelConfig( column_types={"a": "text"}, materialized="table", post_hook=[Hook(sql='insert into blah(a, b) select "1", 1')], @@ -90,11 +91,12 @@ def populated_node_config_dict(): "packages": [], "docs": {"show": True}, "contract": {"enforced": False}, + "access": "protected", } def test_config_populated(populated_node_config_object, populated_node_config_dict): - assert_symmetric(populated_node_config_object, populated_node_config_dict, NodeConfig) + assert_symmetric(populated_node_config_object, populated_node_config_dict, ModelConfig) pickle.loads(pickle.dumps(populated_node_config_object)) @@ -127,14 +129,14 @@ def unrendered_node_config_dict(): @pytest.mark.parametrize("func", different_node_configs) def test_config_different(unrendered_node_config_dict, func): value = func(unrendered_node_config_dict) - assert not NodeConfig.same_contents(unrendered_node_config_dict, value) + assert not ModelConfig.same_contents(unrendered_node_config_dict, value) @pytest.mark.parametrize("func", same_node_configs) def test_config_same(unrendered_node_config_dict, func): value = func(unrendered_node_config_dict) assert unrendered_node_config_dict != value - assert NodeConfig.same_contents(unrendered_node_config_dict, value) + assert ModelConfig.same_contents(unrendered_node_config_dict, value) @pytest.fixture @@ -175,6 +177,7 @@ def base_parsed_model_dict(): "docs": {"show": True}, "contract": {"enforced": False}, "packages": [], + "access": "protected", }, "deferred": False, "docs": {"show": True}, @@ -213,7 +216,7 @@ def basic_parsed_model_object(): schema="test_schema", alias="bar", tags=[], - config=NodeConfig(), + config=ModelConfig(), meta={}, checksum=FileHash.from_contents(""), created_at=1.0, @@ -284,6 +287,7 @@ def complex_parsed_model_dict(): "docs": {"show": True}, "contract": {"enforced": False}, "packages": [], + "access": "protected", }, "docs": {"show": True}, "contract": {"enforced": False}, @@ -334,7 +338,7 @@ def complex_parsed_model_object(): alias="bar", tags=["tag"], meta={}, - config=NodeConfig( + config=ModelConfig( column_types={"a": "text"}, materialized="ephemeral", post_hook=[Hook(sql='insert into blah(a, b) select "1", 1')], diff --git a/tests/unit/test_manifest.py b/tests/unit/test_manifest.py index f6a510ca582..ad651a1858b 100644 --- a/tests/unit/test_manifest.py +++ b/tests/unit/test_manifest.py @@ -19,7 +19,7 @@ from dbt.contracts.graph.nodes import ( ModelNode, DependsOn, - NodeConfig, + ModelConfig, SeedNode, SourceDefinition, Exposure, @@ -112,7 +112,7 @@ def setUp(self): self.maxDiff = None - self.model_config = NodeConfig.from_dict( + self.model_config = ModelConfig.from_dict( { "enabled": True, "materialized": "view", @@ -669,7 +669,7 @@ class MixedManifestTest(unittest.TestCase): def setUp(self): self.maxDiff = None - self.model_config = NodeConfig.from_dict( + self.model_config = ModelConfig.from_dict( { "enabled": True, "materialized": "view", diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index 64bed3825ce..28d6b1f1e93 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -11,7 +11,7 @@ from dbt.context.context_config import ContextConfig from dbt.contracts.files import SourceFile, FileHash, FilePath, SchemaSourceFile from dbt.contracts.graph.manifest import Manifest -from dbt.contracts.graph.model_config import NodeConfig, TestConfig, SnapshotConfig +from dbt.contracts.graph.model_config import NodeConfig, TestConfig, SnapshotConfig, ModelConfig from dbt.contracts.graph.nodes import ( ModelNode, Macro, @@ -928,7 +928,7 @@ def test_basic(self): fqn=["snowplow", "nested", "model_1"], package_name="snowplow", original_file_path=normalize("models/nested/model_1.sql"), - config=NodeConfig(materialized="table"), + config=ModelConfig(materialized="table"), path=normalize("nested/model_1.sql"), language="sql", raw_code=sql_model, @@ -966,7 +966,7 @@ def test_python_model_parse(self): fqn=["snowplow", "nested", "py_model"], package_name="snowplow", original_file_path=normalize("models/nested/py_model.py"), - config=NodeConfig(materialized="table", packages=python_packages), + config=ModelConfig(materialized="table", packages=python_packages), # config.packages = ['textblob'] path=normalize("nested/py_model.py"), language="python", @@ -1123,7 +1123,7 @@ def test_built_in_macro_override_detection(self): fqn=["snowplow", "nested", "model_1"], package_name="snowplow", original_file_path=normalize("models/nested/model_1.sql"), - config=NodeConfig(materialized="table"), + config=ModelConfig(materialized="table"), path=normalize("nested/model_1.sql"), language="sql", raw_code=raw_code, @@ -1159,7 +1159,7 @@ def setUp(self): fqn=["snowplow", "nested", "model_1"], package_name="snowplow", original_file_path=normalize("models/nested/model_1.sql"), - config=NodeConfig(materialized="table"), + config=ModelConfig(materialized="table"), path=normalize("nested/model_1.sql"), language="sql", raw_code='{{ config(materialized="table") }}select 1 as id', From 89946260ebec4a852ecbf730b870a9ce3e852632 Mon Sep 17 00:00:00 2001 From: Gerda Shank Date: Tue, 12 Sep 2023 17:23:38 -0400 Subject: [PATCH 2/4] Fix list test --- tests/functional/list/test_list.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/functional/list/test_list.py b/tests/functional/list/test_list.py index 19739054a40..f6db7461274 100644 --- a/tests/functional/list/test_list.py +++ b/tests/functional/list/test_list.py @@ -182,6 +182,7 @@ def expect_model_output(self): "incremental_strategy": None, "docs": {"node_color": None, "show": True}, "contract": {"enforced": False}, + "access": "protected", }, "original_file_path": normalize("models/ephemeral.sql"), "unique_id": "model.test.ephemeral", @@ -219,6 +220,7 @@ def expect_model_output(self): "incremental_strategy": "delete+insert", "docs": {"node_color": None, "show": True}, "contract": {"enforced": False}, + "access": "protected", }, "original_file_path": normalize("models/incremental.sql"), "unique_id": "model.test.incremental", @@ -256,6 +258,7 @@ def expect_model_output(self): "incremental_strategy": None, "docs": {"node_color": None, "show": True}, "contract": {"enforced": False}, + "access": "protected", }, "original_file_path": normalize("models/sub/inner.sql"), "unique_id": "model.test.inner", @@ -293,6 +296,7 @@ def expect_model_output(self): "incremental_strategy": None, "docs": {"node_color": None, "show": True}, "contract": {"enforced": False}, + "access": "protected", }, "original_file_path": normalize("models/outer.sql"), "unique_id": "model.test.outer", @@ -340,6 +344,7 @@ def expect_model_ephemeral_output(self): "packages": [], "incremental_strategy": None, "docs": {"node_color": None, "show": True}, + "access": "protected", }, "unique_id": "model.test.ephemeral", "original_file_path": normalize("models/ephemeral.sql"), From 14cd9136e19555817eaab6fcda0cce42a2cd6763 Mon Sep 17 00:00:00 2001 From: Gerda Shank Date: Wed, 13 Sep 2023 10:35:59 -0400 Subject: [PATCH 3/4] Add tests --- tests/functional/access/test_access.py | 81 ++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/tests/functional/access/test_access.py b/tests/functional/access/test_access.py index 424616970f7..53cb1926e5e 100644 --- a/tests/functional/access/test_access.py +++ b/tests/functional/access/test_access.py @@ -104,9 +104,6 @@ group: analytics - name: another_model description: "yet another model" - - name: ref_my_model - description: "a model that refs my_model" - group: marts - name: ref_my_model description: "a model that refs my_model" group: analytics @@ -116,6 +113,26 @@ group: analytics """ +v6_schema_yml = """ +models: + - name: my_model + description: "my model" + config: + access: private + group: analytics + - name: another_model + description: "yet another model" + - name: ref_my_model + description: "a model that refs my_model" + config: + group: analytics + - name: people_model + description: "some people" + config: + access: public + group: analytics +""" + people_model_sql = """ select 1 as id, 'Drew' as first_name, 'Banin' as last_name, 'yellow' as favorite_color, true as loves_dbt, 5 as tenure, current_timestamp as created_at union all @@ -313,10 +330,23 @@ def test_access_attribute(self, project): # Should succeed manifest = run_dbt(["parse"]) assert len(manifest.nodes) == 5 - manifest = get_manifest(project.project_root) metric_id = "metric.test.number_of_people" assert manifest.metrics[metric_id].group == "analytics" + # Use access and group in config + write_file(v5_schema_yml, project.project_root, "models", "schema.yml") + manifest = run_dbt(["parse"]) + # ['model.test.another_model', 'model.test.my_model', 'model.test.ref_my_model', 'model.test.people_model', 'model.test.metricflow_time_spine'] + assert len(manifest.nodes) == 5 + assert manifest.nodes["model.test.my_model"].access == AccessType.Private + assert manifest.nodes["model.test.my_model"].group == "analytics" + assert manifest.nodes["model.test.ref_my_model"].access == AccessType.Protected + assert manifest.nodes["model.test.ref_my_model"].group == "analytics" + assert manifest.nodes["model.test.people_model"].access == AccessType.Public + assert manifest.nodes["model.test.people_model"].group == "analytics" + assert manifest.nodes["model.test.another_model"].access == AccessType.Protected + assert manifest.nodes["model.test.another_model"].group is None + class TestUnrestrictedPackageAccess: @pytest.fixture(scope="class", autouse=True) @@ -398,3 +428,46 @@ def test_restricted_private_ref(self, project): with pytest.raises(DbtReferenceError): run_dbt(["parse"]) + + +dbt_project_yml = """ +models: + test: + subdir_one: + +group: analytics + +access: private + subdir_two: + +group: marts + +access: public +""" + + +class TestAccessDbtProjectConfig: + @pytest.fixture(scope="class") + def models(self): + return { + "model_one.sql": my_model_sql, + "subdir_one": { + "model_two.sql": my_model_sql, + }, + "subdir_two": { + "model_three.sql": my_model_sql, + }, + } + + @pytest.fixture(scope="class") + def project_config_update(self): + return dbt_project_yml + + def test_dbt_project_access_config(self, project): + write_file(groups_yml, project.project_root, "models", "groups.yml") + manifest = run_dbt(["parse"]) + model_one = manifest.nodes["model.test.model_one"] + model_two = manifest.nodes["model.test.model_two"] + model_three = manifest.nodes["model.test.model_three"] + assert model_one.group is None + assert model_one.access == AccessType.Protected + assert model_two.group == "analytics" + assert model_two.access == AccessType.Private + assert model_three.group == "marts" + assert model_three.access == AccessType.Public From 1a3c9685d7c53b10b8f914fd6bbcf8d317de1a25 Mon Sep 17 00:00:00 2001 From: Gerda Shank Date: Thu, 14 Sep 2023 10:42:38 -0400 Subject: [PATCH 4/4] Expand changelog comment; use config.get --- .changes/unreleased/Features-20230912-153013.yaml | 2 +- core/dbt/parser/base.py | 6 +----- tests/functional/access/test_access.py | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.changes/unreleased/Features-20230912-153013.yaml b/.changes/unreleased/Features-20230912-153013.yaml index fc3484d6474..8def6f4878a 100644 --- a/.changes/unreleased/Features-20230912-153013.yaml +++ b/.changes/unreleased/Features-20230912-153013.yaml @@ -1,5 +1,5 @@ kind: Features -body: Allow setting access in config +body: Allow setting "access" as a config in addition to as a property time: 2023-09-12T15:30:13.859595-04:00 custom: Author: gshank diff --git a/core/dbt/parser/base.py b/core/dbt/parser/base.py index 740bce7277b..dde4aa5035c 100644 --- a/core/dbt/parser/base.py +++ b/core/dbt/parser/base.py @@ -334,11 +334,7 @@ def update_parsed_node_config( parsed_node.group = config_dict["group"] # If we have access in the config, copy to node level - if ( - parsed_node.resource_type == NodeType.Model - and "access" in config_dict - and config_dict["access"] - ): + if parsed_node.resource_type == NodeType.Model and config_dict.get("access", None): if AccessType.is_valid(config_dict["access"]): parsed_node.access = AccessType(config_dict["access"]) else: diff --git a/tests/functional/access/test_access.py b/tests/functional/access/test_access.py index 53cb1926e5e..8fa4f791d0f 100644 --- a/tests/functional/access/test_access.py +++ b/tests/functional/access/test_access.py @@ -336,7 +336,6 @@ def test_access_attribute(self, project): # Use access and group in config write_file(v5_schema_yml, project.project_root, "models", "schema.yml") manifest = run_dbt(["parse"]) - # ['model.test.another_model', 'model.test.my_model', 'model.test.ref_my_model', 'model.test.people_model', 'model.test.metricflow_time_spine'] assert len(manifest.nodes) == 5 assert manifest.nodes["model.test.my_model"].access == AccessType.Private assert manifest.nodes["model.test.my_model"].group == "analytics"