From c0d6ee42c893950d0d11dff70727a8cef790e2f6 Mon Sep 17 00:00:00 2001 From: vaibhavatlan Date: Thu, 14 May 2026 13:40:21 +0530 Subject: [PATCH 1/3] fix: default daap_visibility on DataProduct.creator to fix blank Overview Assets count (BLDX-1252) Data products created via DataProduct.creator() shipped with daapVisibility=null, which made the marketplace Overview tile render a blank Assets count even when the asset DSL correctly matched. Default daap_visibility to PRIVATE (mirroring the Atlan UI) and expose optional overrides for daap_visibility, daap_visibility_users/groups, and owner_users/groups on both creator() and the deprecated create(). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../methods/asset/data_product.jinja2 | 20 +++++++++++++ .../methods/attribute/data_product.jinja2 | 10 +++++++ pyatlan/model/assets/core/data_product.py | 30 +++++++++++++++++++ tests/unit/model/data_product_test.py | 29 +++++++++++++++++- 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/pyatlan/generator/templates/methods/asset/data_product.jinja2 b/pyatlan/generator/templates/methods/asset/data_product.jinja2 index d5161dedd..b98e051a0 100644 --- a/pyatlan/generator/templates/methods/asset/data_product.jinja2 +++ b/pyatlan/generator/templates/methods/asset/data_product.jinja2 @@ -7,11 +7,21 @@ 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 = 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 +33,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: warn( ( @@ -36,6 +51,11 @@ 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, ) @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..b5514a255 100644 --- a/pyatlan/model/assets/core/data_product.py +++ b/pyatlan/model/assets/core/data_product.py @@ -48,11 +48,21 @@ def creator( 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 = 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 +74,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: warn( ( @@ -77,6 +92,11 @@ def 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, ) @classmethod @@ -633,6 +653,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 +679,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/unit/model/data_product_test.py b/tests/unit/model/data_product_test.py index c22d22e12..5b4df019a 100644 --- a/tests/unit/model/data_product_test.py +++ b/tests/unit/model/data_product_test.py @@ -6,7 +6,11 @@ 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 +125,31 @@ 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_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 +173,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(): From 1d7ea4b8ce0dab30887066c262b0875df09ef72d Mon Sep 17 00:00:00 2001 From: vaibhavatlan Date: Thu, 14 May 2026 13:47:38 +0530 Subject: [PATCH 2/3] fix: default owner_users to calling user when client passed to DataProduct.creator Mirrors the marketplace UI behavior: when a creator is opened in the UI, Visibility is preset to Private and Owners is preset to the calling user. The earlier change defaulted visibility; this adds an optional client kwarg that, when provided and owner_users is not set, resolves the current user via client.user.get_current() and seeds owner_users. Explicit owner_users still wins over the client-derived default. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../methods/asset/data_product.jinja2 | 9 +++++ pyatlan/model/assets/core/data_product.py | 9 +++++ tests/unit/model/data_product_test.py | 34 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/pyatlan/generator/templates/methods/asset/data_product.jinja2 b/pyatlan/generator/templates/methods/asset/data_product.jinja2 index b98e051a0..87b068562 100644 --- a/pyatlan/generator/templates/methods/asset/data_product.jinja2 +++ b/pyatlan/generator/templates/methods/asset/data_product.jinja2 @@ -7,12 +7,19 @@ 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, @@ -33,6 +40,7 @@ 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, @@ -51,6 +59,7 @@ 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, diff --git a/pyatlan/model/assets/core/data_product.py b/pyatlan/model/assets/core/data_product.py index b5514a255..fd8384329 100644 --- a/pyatlan/model/assets/core/data_product.py +++ b/pyatlan/model/assets/core/data_product.py @@ -48,12 +48,19 @@ 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, @@ -74,6 +81,7 @@ 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, @@ -92,6 +100,7 @@ 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, diff --git a/tests/unit/model/data_product_test.py b/tests/unit/model/data_product_test.py index 5b4df019a..1f38aabb6 100644 --- a/tests/unit/model/data_product_test.py +++ b/tests/unit/model/data_product_test.py @@ -1,5 +1,6 @@ from json import dumps, load, loads from pathlib import Path +from unittest.mock import MagicMock import pytest @@ -130,6 +131,39 @@ def test_create( _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, ): From 159ed316504cdd99a4e56d3e6df1902702d25611 Mon Sep 17 00:00:00 2001 From: vaibhavatlan Date: Thu, 14 May 2026 19:47:59 +0530 Subject: [PATCH 3/3] test(integration): add DataProduct default visibility/owner regression guards Asserts the BLDX-1252 fix at the integration layer: a DataProduct created via the default path must come back with daap_visibility=PRIVATE, daap_status=ACTIVE, and owner_users seeded with the calling user. The visibility/status pair is also asserted after a server round-trip in test_retrieve_product to guarantee the server persists both fields. The existing product fixture now passes client=client so the owner default path is exercised. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/integration/data_mesh_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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")