Skip to content

Commit 4f99bc0

Browse files
committed
APP-6823: Added IndistinctAsset to support unmodeled asset types in the SDK
This change introduces a fallback asset model `IndistinctAsset` to gracefully handle assets that are not explicitly modeled in the SDK. Instead of returning `None` when encountering unknown asset types (typically due to newly introduced typedefs or customer-specific types), the SDK will now return an `IndistinctAsset` object containing basic asset fields such as guid, qualifiedName, and typeName.
1 parent 1172fdf commit 4f99bc0

File tree

6 files changed

+125
-1
lines changed

6 files changed

+125
-1
lines changed

pyatlan/model/assets/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"MCMonitor",
119119
"SodaCheck",
120120
"SnowflakeDynamicTable",
121+
"IndistinctAsset",
121122
],
122123
"task": ["Task"],
123124
"form": ["Form"],

pyatlan/model/assets/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ __all__ = [
341341
"AzureEventHubConsumerGroup",
342342
"DynamoDBLocalSecondaryIndex",
343343
"DynamoDBGlobalSecondaryIndex",
344+
"IndistinctAsset",
344345
]
345346

346347
from .a_d_l_s import ADLS
@@ -460,6 +461,7 @@ from .core.fivetran import Fivetran
460461
from .core.fivetran_connector import FivetranConnector
461462
from .core.folder import Folder
462463
from .core.function import Function
464+
from .core.indistinct_asset import IndistinctAsset
463465
from .core.link import Link
464466
from .core.m_c_incident import MCIncident
465467
from .core.m_c_monitor import MCMonitor

pyatlan/model/assets/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from .fivetran_connector import FivetranConnector
5555
from .folder import Folder
5656
from .function import Function
57+
from .indistinct_asset import IndistinctAsset # noqa: F401
5758
from .link import Link
5859
from .m_c_incident import MCIncident
5960
from .m_c_monitor import MCMonitor

pyatlan/model/assets/core/asset.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ def __get_validators__(cls):
148148

149149
@classmethod
150150
def _convert_to_real_type_(cls, data):
151+
from .indistinct_asset import IndistinctAsset
152+
151153
"""Convert raw asset data into the appropriate asset type."""
152154
if isinstance(data, Asset):
153155
return data
@@ -167,7 +169,9 @@ def _convert_to_real_type_(cls, data):
167169
sys.modules.get("pyatlan.model.assets", {}), data_type, None
168170
)
169171

170-
return sub_type(**data) if sub_type else None # Gracefully handle missing types
172+
return (
173+
sub_type(**data) if sub_type else IndistinctAsset(**data)
174+
) # If no subtype found, return IndistinctAsset
171175

172176
if TYPE_CHECKING:
173177
from pyatlan.model.lineage import FluentLineage
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2025 Atlan Pte. Ltd.
3+
4+
from __future__ import annotations
5+
6+
from pydantic.v1 import Field, validator
7+
8+
from .asset import Asset
9+
10+
11+
class IndistinctAsset(Asset, type_name="IndistinctAsset"):
12+
"""
13+
Instance of an asset where we cannot determine
14+
(have not yet modeled) its detailed information.
15+
In the meanwhile, this provides a catch-all case
16+
where at least the basic asset information is available.
17+
"""
18+
19+
type_name: str = Field(default="IndistinctAsset", allow_mutation=True)
20+
21+
@validator("type_name")
22+
def validate_type_name(cls, v):
23+
return v

tests/unit/test_model.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
from inspect import signature
66
from pathlib import Path
77
from re import escape
8+
from typing import List
89
from unittest.mock import create_autospec
910

1011
import pytest
12+
from pydantic.v1 import parse_obj_as
1113
from pydantic.v1.error_wrappers import ValidationError
1214

1315
from pyatlan.client.atlan import AtlanClient
@@ -57,6 +59,7 @@
5759
Function,
5860
GCSBucket,
5961
GCSObject,
62+
IndistinctAsset,
6063
KafkaConsumerGroup,
6164
KafkaTopic,
6265
Link,
@@ -975,3 +978,93 @@ def test_tableau_upstream_fields_deserialization(test_data):
975978
**{"typeName": "TableauDatasourceField", "attributes": test_data}
976979
)
977980
assert tdf.upstream_tables == test_data["upstreamTables"]
981+
982+
983+
def test_indistict_asset_type_deserialization():
984+
data = {
985+
"entities": [
986+
{
987+
"typeName": "UnknownAsset1",
988+
"attributes": {
989+
"name": "Batman",
990+
"connectorName": "mssql",
991+
"ownerUsers": ["admin1"],
992+
"qualifiedName": "default/mssql/1709355572/Batman",
993+
"meanings": [
994+
{
995+
"guid": "728d8571-a4fc-42ed-8d47-62377384570c",
996+
"typeName": "AtlasGlossaryTerm",
997+
"attributes": {"name": "test-term1"},
998+
"uniqueAttributes": {"qualifiedName": "test-term1-qn"},
999+
}
1000+
],
1001+
},
1002+
"guid": "558c5cc8-0b18-4b27-b7c7-0c1693c9d1e5",
1003+
"isIncomplete": False,
1004+
"status": "ACTIVE",
1005+
"createdBy": "service-account-apikey-123",
1006+
"updatedBy": "service-account-apikey-123",
1007+
"createTime": 1709355972663,
1008+
"updateTime": 1709356203915,
1009+
"version": 0,
1010+
"labels": [],
1011+
},
1012+
{
1013+
"typeName": "UnknownAsset2",
1014+
"attributes": {
1015+
"name": "Superman",
1016+
"connectorName": "sqlserver",
1017+
"ownerUsers": ["admin2"],
1018+
"qualifiedName": "default/mssql/1709355572/Superman",
1019+
"meanings": [
1020+
{
1021+
"guid": "828d8571-a4fc-42ed-8d47-62377384570c",
1022+
"typeName": "AtlasGlossaryTerm",
1023+
"attributes": {"name": "test-term21"},
1024+
"uniqueAttributes": {"qualifiedName": "test-term21-qn"},
1025+
},
1026+
{
1027+
"guid": "928d8571-a4fc-42ed-8d47-62377384570c",
1028+
"typeName": "AtlasGlossaryTerm",
1029+
"attributes": {"name": "test-term22"},
1030+
"uniqueAttributes": {"qualifiedName": "test-term22-qn"},
1031+
},
1032+
],
1033+
},
1034+
"guid": "458c5cc8-0b18-4b27-b7c7-0c1693c9d1e5",
1035+
"isIncomplete": False,
1036+
"status": "ACTIVE",
1037+
"createdBy": "service-account-apikey-123",
1038+
"updatedBy": "service-account-apikey-123",
1039+
"createTime": 1709355972663,
1040+
"updateTime": 1709356203915,
1041+
"version": 0,
1042+
"labels": [],
1043+
},
1044+
]
1045+
}
1046+
1047+
assets = parse_obj_as(List[Asset], data["entities"])
1048+
assert (
1049+
len(assets) == 2
1050+
and isinstance(assets[0], IndistinctAsset)
1051+
and isinstance(assets[1], IndistinctAsset)
1052+
)
1053+
1054+
assert assets[0].type_name == "UnknownAsset1"
1055+
assert assets[0].name == "Batman"
1056+
assert assets[0].connector_name == "mssql"
1057+
assert assets[0].owner_users == {"admin1"}
1058+
assert len(assets[0].assigned_terms) == 1
1059+
assert assets[0].assigned_terms[0].name == "test-term1"
1060+
assert assets[0].assigned_terms[0].qualified_name == "test-term1-qn"
1061+
1062+
assert assets[1].type_name == "UnknownAsset2"
1063+
assert assets[1].name == "Superman"
1064+
assert assets[1].connector_name == "sqlserver"
1065+
assert assets[1].owner_users == {"admin2"}
1066+
assert len(assets[1].assigned_terms) == 2
1067+
assert assets[1].assigned_terms[0].name == "test-term21"
1068+
assert assets[1].assigned_terms[0].qualified_name == "test-term21-qn"
1069+
assert assets[1].assigned_terms[1].name == "test-term22"
1070+
assert assets[1].assigned_terms[1].qualified_name == "test-term22-qn"

0 commit comments

Comments
 (0)