diff --git a/pyatlan/generator/templates/methods/asset/data_product.jinja2 b/pyatlan/generator/templates/methods/asset/data_product.jinja2 index d5161dedd..87b068562 100644 --- a/pyatlan/generator/templates/methods/asset/data_product.jinja2 +++ b/pyatlan/generator/templates/methods/asset/data_product.jinja2 @@ -7,11 +7,28 @@ name: StrictStr, domain_qualified_name: StrictStr, asset_selection: IndexSearchRequest, + client: Optional[AtlanClient] = None, + daap_visibility: Optional[DataProductVisibility] = None, + daap_visibility_users: Optional[Set[str]] = None, + daap_visibility_groups: Optional[Set[str]] = None, + owner_users: Optional[Set[str]] = None, + owner_groups: Optional[Set[str]] = None, ) -> DataProduct: + # Mirror UI default: when a client is provided and no owners are + # specified, seed owner_users with the calling user. + if client is not None and owner_users is None: + current_username = client.user.get_current().username + if current_username: + owner_users = {current_username} attributes = DataProduct.Attributes.create( name=name, domain_qualified_name=domain_qualified_name, asset_selection=asset_selection, + daap_visibility=daap_visibility, + daap_visibility_users=daap_visibility_users, + daap_visibility_groups=daap_visibility_groups, + owner_users=owner_users, + owner_groups=owner_groups, ) return cls(attributes=attributes) @@ -23,6 +40,12 @@ name: StrictStr, domain_qualified_name: StrictStr, asset_selection: IndexSearchRequest, + client: Optional[AtlanClient] = None, + daap_visibility: Optional[DataProductVisibility] = None, + daap_visibility_users: Optional[Set[str]] = None, + daap_visibility_groups: Optional[Set[str]] = None, + owner_users: Optional[Set[str]] = None, + owner_groups: Optional[Set[str]] = None, ) -> DataProduct: warn( ( @@ -36,6 +59,12 @@ name=name, domain_qualified_name=domain_qualified_name, asset_selection=asset_selection, + client=client, + daap_visibility=daap_visibility, + daap_visibility_users=daap_visibility_users, + daap_visibility_groups=daap_visibility_groups, + owner_users=owner_users, + owner_groups=owner_groups, ) @classmethod diff --git a/pyatlan/generator/templates/methods/attribute/data_product.jinja2 b/pyatlan/generator/templates/methods/attribute/data_product.jinja2 index ae0bf85de..c4ca3f110 100644 --- a/pyatlan/generator/templates/methods/attribute/data_product.jinja2 +++ b/pyatlan/generator/templates/methods/attribute/data_product.jinja2 @@ -7,6 +7,11 @@ name: StrictStr, domain_qualified_name: StrictStr, asset_selection: IndexSearchRequest, + daap_visibility: Optional[DataProductVisibility] = None, + daap_visibility_users: Optional[Set[str]] = None, + daap_visibility_groups: Optional[Set[str]] = None, + owner_users: Optional[Set[str]] = None, + owner_groups: Optional[Set[str]] = None, ) -> DataProduct.Attributes: validate_required_fields( ["name", "domain_qualified_name", "asset_selection"], @@ -28,4 +33,9 @@ domain_qualified_name ), daap_status=DataProductStatus.ACTIVE, + daap_visibility=daap_visibility or DataProductVisibility.PRIVATE, + daap_visibility_users=daap_visibility_users, + daap_visibility_groups=daap_visibility_groups, + owner_users=owner_users, + owner_groups=owner_groups, ) diff --git a/pyatlan/model/assets/core/data_product.py b/pyatlan/model/assets/core/data_product.py index e2ad46d40..fd8384329 100644 --- a/pyatlan/model/assets/core/data_product.py +++ b/pyatlan/model/assets/core/data_product.py @@ -48,11 +48,28 @@ def creator( name: StrictStr, domain_qualified_name: StrictStr, asset_selection: IndexSearchRequest, + client: Optional[AtlanClient] = None, + daap_visibility: Optional[DataProductVisibility] = None, + daap_visibility_users: Optional[Set[str]] = None, + daap_visibility_groups: Optional[Set[str]] = None, + owner_users: Optional[Set[str]] = None, + owner_groups: Optional[Set[str]] = None, ) -> DataProduct: + # Mirror UI default: when a client is provided and no owners are + # specified, seed owner_users with the calling user. + if client is not None and owner_users is None: + current_username = client.user.get_current().username + if current_username: + owner_users = {current_username} attributes = DataProduct.Attributes.create( name=name, domain_qualified_name=domain_qualified_name, asset_selection=asset_selection, + daap_visibility=daap_visibility, + daap_visibility_users=daap_visibility_users, + daap_visibility_groups=daap_visibility_groups, + owner_users=owner_users, + owner_groups=owner_groups, ) return cls(attributes=attributes) @@ -64,6 +81,12 @@ def create( name: StrictStr, domain_qualified_name: StrictStr, asset_selection: IndexSearchRequest, + client: Optional[AtlanClient] = None, + daap_visibility: Optional[DataProductVisibility] = None, + daap_visibility_users: Optional[Set[str]] = None, + daap_visibility_groups: Optional[Set[str]] = None, + owner_users: Optional[Set[str]] = None, + owner_groups: Optional[Set[str]] = None, ) -> DataProduct: warn( ( @@ -77,6 +100,12 @@ def create( name=name, domain_qualified_name=domain_qualified_name, asset_selection=asset_selection, + client=client, + daap_visibility=daap_visibility, + daap_visibility_users=daap_visibility_users, + daap_visibility_groups=daap_visibility_groups, + owner_users=owner_users, + owner_groups=owner_groups, ) @classmethod @@ -633,6 +662,11 @@ def create( name: StrictStr, domain_qualified_name: StrictStr, asset_selection: IndexSearchRequest, + daap_visibility: Optional[DataProductVisibility] = None, + daap_visibility_users: Optional[Set[str]] = None, + daap_visibility_groups: Optional[Set[str]] = None, + owner_users: Optional[Set[str]] = None, + owner_groups: Optional[Set[str]] = None, ) -> DataProduct.Attributes: validate_required_fields( ["name", "domain_qualified_name", "asset_selection"], @@ -654,6 +688,11 @@ def create( domain_qualified_name ), daap_status=DataProductStatus.ACTIVE, + daap_visibility=daap_visibility or DataProductVisibility.PRIVATE, + daap_visibility_users=daap_visibility_users, + daap_visibility_groups=daap_visibility_groups, + owner_users=owner_users, + owner_groups=owner_groups, ) attributes: DataProduct.Attributes = Field( diff --git a/tests/integration/data_mesh_test.py b/tests/integration/data_mesh_test.py index d78feee29..68ae5f000 100644 --- a/tests/integration/data_mesh_test.py +++ b/tests/integration/data_mesh_test.py @@ -25,6 +25,7 @@ CertificateStatus, DataContractStatus, DataProductStatus, + DataProductVisibility, EntityStatus, ) from pyatlan.model.fluent_search import FluentSearch @@ -266,6 +267,7 @@ def product( name=DATA_PRODUCT_NAME, asset_selection=assets, domain_qualified_name=domain.qualified_name, + client=client, ) product.output_ports = [table] response = client.asset.save(product) @@ -288,6 +290,14 @@ def test_product(client: AtlanClient, product: DataProduct): assert re.search(DATA_PRODUCT_QN_REGEX, product.qualified_name) assert re.search(DATA_DOMAIN_QN_REGEX, product.parent_domain_qualified_name) assert re.search(DATA_DOMAIN_QN_REGEX, product.super_domain_qualified_name) + # BLDX-1252: default-path visibility must be PRIVATE and status ACTIVE so + # the marketplace Overview "Assets" tile renders. + assert product.daap_visibility == DataProductVisibility.PRIVATE + assert product.daap_status == DataProductStatus.ACTIVE + # BLDX-1252: when client is passed, owner_users defaults to the calling user. + current_username = client.user.get_current().username + assert current_username + assert product.owner_users == {current_username} @pytest.fixture(scope="module") @@ -457,6 +467,10 @@ def test_retrieve_product(client: AtlanClient, product: DataProduct): assert test_product.name == product.name assert test_product.certificate_status == CERTIFICATE_STATUS assert test_product.certificate_status_message == CERTIFICATE_MESSAGE + # BLDX-1252: server must persist the default daap_visibility/daap_status. + # test_update_product does not touch either field. + assert test_product.daap_visibility == DataProductVisibility.PRIVATE + assert test_product.daap_status == DataProductStatus.ACTIVE @pytest.mark.order(after="test_update_contract") diff --git a/tests/unit/model/data_product_test.py b/tests/unit/model/data_product_test.py index c22d22e12..1f38aabb6 100644 --- a/tests/unit/model/data_product_test.py +++ b/tests/unit/model/data_product_test.py @@ -1,12 +1,17 @@ from json import dumps, load, loads from pathlib import Path +from unittest.mock import MagicMock import pytest from pyatlan.client.atlan import AtlanClient from pyatlan.errors import InvalidRequestError from pyatlan.model.assets import AtlasGlossary, DataProduct -from pyatlan.model.enums import CertificateStatus, DataProductStatus +from pyatlan.model.enums import ( + CertificateStatus, + DataProductStatus, + DataProductVisibility, +) from pyatlan.model.fluent_search import CompoundQuery, FluentSearch from pyatlan.model.search import IndexSearchRequest from tests.unit.model.constants import ( @@ -121,9 +126,64 @@ def test_create( expected_asset_dsl = dumps(data_product_assets_dsl_json, sort_keys=True) assert test_asset_dsl == expected_asset_dsl assert test_product.data_product_assets_playbook_filter == ASSETS_PLAYBOOK_FILTER + assert test_product.daap_status == DataProductStatus.ACTIVE + assert test_product.daap_visibility == DataProductVisibility.PRIVATE _assert_product(test_product) +def test_create_defaults_owner_users_to_current_user_when_client_provided( + data_product_asset_selection: IndexSearchRequest, +): + mock_client = MagicMock(spec=AtlanClient) + mock_client.user.get_current.return_value = MagicMock(username="calling-user") + + test_product = DataProduct.creator( + name=DATA_PRODUCT_NAME, + asset_selection=data_product_asset_selection, + domain_qualified_name=DATA_DOMAIN_QUALIFIED_NAME, + client=mock_client, + ) + assert test_product.owner_users == {"calling-user"} + assert test_product.daap_visibility == DataProductVisibility.PRIVATE + + +def test_create_explicit_owner_users_takes_precedence_over_client( + data_product_asset_selection: IndexSearchRequest, +): + mock_client = MagicMock(spec=AtlanClient) + mock_client.user.get_current.return_value = MagicMock(username="calling-user") + + test_product = DataProduct.creator( + name=DATA_PRODUCT_NAME, + asset_selection=data_product_asset_selection, + domain_qualified_name=DATA_DOMAIN_QUALIFIED_NAME, + client=mock_client, + owner_users={"explicit-owner"}, + ) + assert test_product.owner_users == {"explicit-owner"} + mock_client.user.get_current.assert_not_called() + + +def test_create_with_overrides( + data_product_asset_selection: IndexSearchRequest, +): + test_product = DataProduct.create( + name=DATA_PRODUCT_NAME, + asset_selection=data_product_asset_selection, + domain_qualified_name=DATA_DOMAIN_QUALIFIED_NAME, + daap_visibility=DataProductVisibility.PUBLIC, + daap_visibility_users={"user1"}, + daap_visibility_groups={"group1"}, + owner_users={"owner1"}, + owner_groups={"owner_group1"}, + ) + assert test_product.daap_visibility == DataProductVisibility.PUBLIC + assert test_product.daap_visibility_users == {"user1"} + assert test_product.daap_visibility_groups == {"group1"} + assert test_product.owner_users == {"owner1"} + assert test_product.owner_groups == {"owner_group1"} + + def test_create_under_sub_domain( data_product_asset_selection: IndexSearchRequest, data_product_assets_dsl_json ): @@ -147,6 +207,7 @@ def test_create_under_sub_domain( test_product, qualified_name=DATA_PRODUCT_UNDER_SUB_DOMAIN_QUALIFIED_NAME ) assert test_product.daap_status == DataProductStatus.ACTIVE + assert test_product.daap_visibility == DataProductVisibility.PRIVATE def test_create_for_modification():