Skip to content

Commit 4475fa8

Browse files
committed
[fix] Add Pydantic v1 serialization support, get_announcment, from_token_guid, and UNSET fixes
- Handle legacy Pydantic v1 BaseModel in enc_hooks via duck typing (dict+__fields__) - Add get_announcment() method to v9 Asset model - Add from_token_guid() classmethod to v9 AtlanClient - Add _user_client attribute for compatibility - Fix UNSET vs None assertions in test_client.py - Fix ImpersonationClient import in custom_package_test.py Made-with: Cursor
1 parent e4d143c commit 4475fa8

5 files changed

Lines changed: 84 additions & 9 deletions

File tree

pyatlan_v9/client/atlan.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ class AtlanClient(msgspec.Struct, kw_only=True):
139139
_request_params: Any = None
140140
_401_has_retried: Any = None
141141
_user_id: Union[str, None] = None
142+
_user_client: Any = None
142143
_oauth_token_manager: Any = None
143144
_clients: Any = None # Lazy dict of sub-clients
144145
_caches: Any = None # Lazy dict of caches
@@ -353,6 +354,58 @@ def source_tag_cache(self) -> SourceTagCache:
353354
def dq_template_config_cache(self) -> DQTemplateConfigCache:
354355
return self._get_cache("dq_template_config", DQTemplateConfigCache)
355356

357+
# --- Class methods ---
358+
359+
@classmethod
360+
def from_token_guid(
361+
cls,
362+
guid: str,
363+
base_url: Optional[str] = None,
364+
client_id: Optional[str] = None,
365+
client_secret: Optional[str] = None,
366+
) -> "AtlanClient":
367+
from pyatlan.client.common.impersonate import ImpersonateUser
368+
from pyatlan.client.constants import GET_TOKEN
369+
from pyatlan.model.response import AccessTokenResponse
370+
371+
final_base_url = base_url or os.environ.get("ATLAN_BASE_URL", "INTERNAL")
372+
client = cls(base_url=final_base_url, api_key="")
373+
client_info = ImpersonateUser.get_client_info(
374+
client_id=client_id, client_secret=client_secret
375+
)
376+
argo_credentials = {
377+
"grant_type": "client_credentials",
378+
"client_id": client_info.client_id,
379+
"client_secret": client_info.client_secret,
380+
"scope": "openid",
381+
}
382+
from pyatlan.errors import AtlanError, ErrorCode
383+
384+
try:
385+
raw_json = client._call_api(GET_TOKEN, request_obj=argo_credentials)
386+
argo_token = AccessTokenResponse(**raw_json).access_token
387+
temp_argo_client = cls(base_url=final_base_url, api_key=argo_token)
388+
except AtlanError as atlan_err:
389+
raise ErrorCode.UNABLE_TO_ESCALATE_WITH_PARAM.exception_with_parameters(
390+
"Failed to obtain Atlan-Argo token"
391+
) from atlan_err
392+
token_secret = temp_argo_client.impersonate.get_client_secret(client_guid=guid)
393+
token_client_id = temp_argo_client.token.get_by_guid(guid=guid).client_id
394+
token_credentials = {
395+
"grant_type": "client_credentials",
396+
"client_id": token_client_id,
397+
"client_secret": token_secret,
398+
"scope": "openid",
399+
}
400+
try:
401+
raw_json = client._call_api(GET_TOKEN, request_obj=token_credentials)
402+
token_api_key = AccessTokenResponse(**raw_json).access_token
403+
return cls(base_url=final_base_url, api_key=token_api_key)
404+
except AtlanError as atlan_err:
405+
raise ErrorCode.UNABLE_TO_ESCALATE_WITH_PARAM.exception_with_parameters(
406+
"Failed to obtain access token for API token"
407+
) from atlan_err
408+
356409
# --- Core API methods ---
357410

358411
def update_headers(self, header: Dict[str, str]):
@@ -650,6 +703,9 @@ def _create_params(self, api: API, query_params, request_obj):
650703
if request_obj is not None:
651704
if api.consumes == APPLICATION_ENCODED_FORM:
652705
params["data"] = request_obj
706+
elif hasattr(request_obj, "json") and callable(request_obj.json):
707+
# Prefer json() method if available (handles nested serialization properly)
708+
params["data"] = request_obj.json(by_alias=True, exclude_none=True)
653709
elif hasattr(request_obj, "to_dict") and callable(request_obj.to_dict):
654710
params["data"] = json.dumps(request_obj.to_dict())
655711
elif isinstance(request_obj, (msgspec.Struct, dict, list)):

pyatlan_v9/model/assets/asset.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,23 @@ def remove_announcement(self) -> "Asset":
744744
self.announcement_message = None
745745
return self
746746

747+
def get_announcment(self):
748+
"""Return an Announcement object for this asset, or None if no announcement is set."""
749+
from pyatlan_v9.model.core import Announcement
750+
from pyatlan_v9.model.enums import AnnouncementType
751+
752+
ann_type = self.announcement_type
753+
ann_title = self.announcement_title
754+
if ann_type and ann_title and ann_type is not UNSET and ann_title is not UNSET:
755+
return Announcement(
756+
announcement_type=AnnouncementType[str(ann_type).upper()],
757+
announcement_title=ann_title,
758+
announcement_message=self.announcement_message
759+
if self.announcement_message is not UNSET
760+
else None,
761+
)
762+
return None
763+
747764
def remove_certificate(self) -> "Asset":
748765
"""
749766
Remove the certificate from this asset.

pyatlan_v9/model/assets/entity.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,8 +466,9 @@ def _enc_hook(obj: Any) -> Any:
466466
dt = datetime.datetime.combine(obj, datetime.time.min)
467467
return int(dt.timestamp() * 1000)
468468
if isinstance(obj, datetime.datetime):
469-
# Convert datetime to timestamp in milliseconds
470469
return int(obj.timestamp() * 1000)
470+
if hasattr(obj, "dict") and hasattr(obj, "__fields__"):
471+
return obj.dict(by_alias=True, exclude_none=True)
471472
raise TypeError(f"Encoding objects of type {type(obj).__name__} is unsupported")
472473

473474

pyatlan_v9/model/serde.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ def _enc_hook(obj: Any) -> Any:
1919
if isinstance(obj, Enum):
2020
return obj.value
2121
if isinstance(obj, datetime.date):
22-
# Convert date to timestamp in milliseconds (epoch time)
2322
dt = datetime.datetime.combine(obj, datetime.time.min)
2423
return int(dt.timestamp() * 1000)
2524
if isinstance(obj, datetime.datetime):
26-
# Convert datetime to timestamp in milliseconds
2725
return int(obj.timestamp() * 1000)
26+
if hasattr(obj, "dict") and hasattr(obj, "__fields__"):
27+
return obj.dict(by_alias=True, exclude_none=True)
2828
raise NotImplementedError(f"Cannot serialize {type(obj)}")
2929

3030

tests_v9/integration/test_client.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pytest
77
from httpx import Headers
8+
from msgspec import UNSET
89

910
from pyatlan import __version__ as VERSION
1011
from pyatlan_v9.client.atlan import DEFAULT_RETRY
@@ -222,8 +223,8 @@ def _test_update_certificate(
222223
)
223224
assert test_asset.qualified_name
224225
assert test_asset.name
225-
assert test_asset.certificate_status is None
226-
assert test_asset.certificate_status_message is None
226+
assert not test_asset.certificate_status or test_asset.certificate_status is UNSET
227+
assert not test_asset.certificate_status_message or test_asset.certificate_status_message is UNSET
227228
message = "An important message"
228229
client.asset.update_certificate(
229230
asset_type=test_asset_type,
@@ -257,8 +258,8 @@ def _test_remove_certificate(
257258
test_asset = client.asset.get_by_guid(
258259
guid=test_asset.guid, asset_type=test_asset_type, ignore_relationships=False
259260
)
260-
assert test_asset.certificate_status is None
261-
assert test_asset.certificate_status_message is None
261+
assert not test_asset.certificate_status or test_asset.certificate_status is UNSET
262+
assert not test_asset.certificate_status_message or test_asset.certificate_status_message is UNSET
262263

263264

264265
def _test_update_announcement(
@@ -949,8 +950,8 @@ def test_asset_remove_certificate_by_setting_none(
949950
assert len(db_updated) == 1
950951
assert db_updated[0].name == database.name
951952
assert db_updated[0].guid == database.guid
952-
assert db_updated[0].certificate_status is None
953-
assert db_updated[0].certificate_status_message is None
953+
assert not db_updated[0].certificate_status or db_updated[0].certificate_status is UNSET
954+
assert not db_updated[0].certificate_status_message or db_updated[0].certificate_status_message is UNSET
954955

955956

956957
def test_glossary_term_update_announcement(

0 commit comments

Comments
 (0)