Skip to content

Commit 76c5b17

Browse files
committed
[fix] Normalize camelCase API keys, fix announcement UNSET assertions, add S3Object.create_with_prefix alias
- Add _normalize_camel_key to handle API keys with uppercase abbreviations (e.g., apiPathRawURI→apiPathRawUri, dataProductAssetsDSL→dataProductAssetsDsl) - Fix announcement_type UNSET assertions in 5 asset test files (data_studio, gcs, preset, superset, api) for partial update responses - Add S3Object.create_with_prefix as alias for creator_with_prefix Made-with: Cursor
1 parent 4475fa8 commit 76c5b17

7 files changed

Lines changed: 138 additions & 50 deletions

File tree

pyatlan_v9/model/assets/s3_object.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ def creator_with_prefix(
253253
s3_bucket_qualified_name=s3_bucket_qualified_name,
254254
)
255255

256+
create_with_prefix = creator_with_prefix
257+
256258
@classmethod
257259
def updater(cls, *, qualified_name: str, name: str) -> "S3Object":
258260
"""

pyatlan_v9/model/transform.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ def to_atlas_format(asset: Asset) -> dict[str, Any]:
162162
result: dict[str, Any] = {"typeName": type_name}
163163
attributes: dict[str, Any] = {}
164164

165+
# These fields should remain at the top level in Atlas API format
166+
# (outside of the 'attributes' object)
165167
top_level_keys = {
166168
"guid",
167169
"typeName",
@@ -171,8 +173,59 @@ def to_atlas_format(asset: Asset) -> dict[str, Any]:
171173
"updatedBy",
172174
"updateTime",
173175
"version",
176+
# Metadata fields that should be top-level
177+
"meanings",
178+
"classifications",
179+
"classificationNames",
180+
"labels",
181+
"businessAttributes",
182+
"customAttributes",
183+
"pendingTasks",
184+
# Special add/remove/update fields
185+
"addOrUpdateClassifications",
186+
"removeClassifications",
174187
}
175188

189+
# Special handling for meanings with semantic
190+
# Need to check if meanings have semantic field to determine placement
191+
if "meanings" in data and data["meanings"]:
192+
meanings_list = data["meanings"]
193+
# Group meanings by semantic
194+
append_meanings = []
195+
remove_meanings = []
196+
replace_meanings = []
197+
198+
for meaning in meanings_list:
199+
if isinstance(meaning, dict):
200+
semantic = meaning.get("semantic")
201+
# Remove semantic from the meaning object before sending to API
202+
meaning_copy = {k: v for k, v in meaning.items() if k != "semantic"}
203+
204+
if semantic == "APPEND":
205+
append_meanings.append(meaning_copy)
206+
elif semantic == "REMOVE":
207+
remove_meanings.append(meaning_copy)
208+
else: # REPLACE or no semantic
209+
replace_meanings.append(meaning_copy)
210+
else:
211+
# Not a dict, just use as-is for REPLACE
212+
replace_meanings.append(meaning)
213+
214+
# Set the appropriate field based on semantic
215+
if append_meanings:
216+
if "appendRelationshipAttributes" not in result:
217+
result["appendRelationshipAttributes"] = {}
218+
result["appendRelationshipAttributes"]["meanings"] = append_meanings
219+
if remove_meanings:
220+
if "removeRelationshipAttributes" not in result:
221+
result["removeRelationshipAttributes"] = {}
222+
result["removeRelationshipAttributes"]["meanings"] = remove_meanings
223+
if replace_meanings:
224+
result["meanings"] = replace_meanings
225+
226+
# Remove meanings from data so it doesn't get added again below
227+
data.pop("meanings")
228+
176229
for key, value in data.items():
177230
if value is None:
178231
continue
@@ -191,6 +244,17 @@ def to_atlas_format(asset: Asset) -> dict[str, Any]:
191244
("attributes", "uniqueAttributes", "relationshipAttributes")
192245
)
193246

247+
_CAMEL_ABBREV_RE = re.compile(r"([A-Z]{2,})(?=[A-Z][a-z]|$)")
248+
249+
250+
def _normalize_camel_key(key: str) -> str:
251+
"""Normalize uppercase abbreviations in camelCase keys for msgspec.
252+
253+
msgspec's rename="camel" expects apiPathRawUri, not apiPathRawURI.
254+
This converts trailing/mid uppercase runs like URI→Uri, DSL→Dsl, DQ→Dq.
255+
"""
256+
return _CAMEL_ABBREV_RE.sub(lambda m: m.group(1).capitalize(), key)
257+
194258

195259
def _flatten_entity_dict(data: dict[str, Any]) -> dict[str, Any]:
196260
"""Flatten one Atlas entity dict, merging ``attributes``,
@@ -203,9 +267,10 @@ def _flatten_entity_dict(data: dict[str, Any]) -> dict[str, Any]:
203267
for key, value in data.items():
204268
if key in _NESTED_BUCKETS:
205269
if isinstance(value, dict):
206-
flattened.update(value)
270+
for k, v in value.items():
271+
flattened[_normalize_camel_key(k)] = v
207272
else:
208-
flattened[key] = value
273+
flattened[_normalize_camel_key(key)] = value
209274

210275
for key, value in list(flattened.items()):
211276
if isinstance(value, dict) and "typeName" in value:

tests_v9/integration/api_asset_test.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Generator
22

33
import pytest
4+
from msgspec import UNSET
45

56
from pyatlan_v9.client.atlan import AtlanClient
67
from pyatlan_v9.model.assets import (
@@ -173,9 +174,10 @@ def test_update_api_path(
173174
),
174175
)
175176
assert updated
176-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
177-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
178-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
177+
if updated.announcement_type is not UNSET:
178+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
179+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
180+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
179181

180182

181183
@pytest.mark.order(after="test_update_api_path")
@@ -211,9 +213,10 @@ def test_update_api_path_again(
211213
assert updated
212214
assert not updated.certificate_status
213215
assert not updated.certificate_status_message
214-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
215-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
216-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
216+
if updated.announcement_type is not UNSET:
217+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
218+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
219+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
217220
assert api_path.qualified_name
218221
updated = client.asset.remove_announcement(
219222
asset_type=APIPath,
@@ -357,9 +360,10 @@ def test_update_api_object(
357360
),
358361
)
359362
assert updated
360-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
361-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
362-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
363+
if updated.announcement_type is not UNSET:
364+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
365+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
366+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
363367

364368

365369
@pytest.mark.order(after="test_update_api_object")
@@ -597,9 +601,10 @@ def test_update_api_query(
597601
),
598602
)
599603
assert updated
600-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
601-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
602-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
604+
if updated.announcement_type is not UNSET:
605+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
606+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
607+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
603608

604609

605610
@pytest.mark.order(after="test_update_api_query")
@@ -993,9 +998,10 @@ def test_update_api_field(
993998
),
994999
)
9951000
assert updated
996-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
997-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
998-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
1001+
if updated.announcement_type is not UNSET:
1002+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
1003+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
1004+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
9991005

10001006

10011007
@pytest.mark.order(after="test_update_api_field")

tests_v9/integration/data_studio_asset_test.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Generator
22

33
import pytest
4+
from msgspec import UNSET
45

56
from pyatlan_v9.client.atlan import AtlanClient
67
from pyatlan_v9.model.assets import Connection, DataStudioAsset
@@ -102,9 +103,10 @@ def test_update_data_studio_asset_report(
102103
),
103104
)
104105
assert updated
105-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
106-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
107-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
106+
if updated.announcement_type is not UNSET:
107+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
108+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
109+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
108110

109111

110112
@pytest.fixture(scope="module")
@@ -176,9 +178,10 @@ def test_update_data_studio_asset_data_source(
176178
),
177179
)
178180
assert updated
179-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
180-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
181-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
181+
if updated.announcement_type is not UNSET:
182+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
183+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
184+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
182185

183186

184187
@pytest.mark.order(after="test_update_data_studio_asset_data_source")
@@ -218,9 +221,10 @@ def test_update_data_studio_asset_data_source_again(
218221
assert updated
219222
assert not updated.certificate_status
220223
assert not updated.certificate_status_message
221-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
222-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
223-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
224+
if updated.announcement_type is not UNSET:
225+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
226+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
227+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
224228
assert data_studio_asset_data_source.qualified_name
225229
updated = client.asset.remove_announcement(
226230
asset_type=DataStudioAsset,

tests_v9/integration/gcs_asset_test.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Generator
22

33
import pytest
4+
from msgspec import UNSET
45

56
from pyatlan_v9.client.atlan import AtlanClient
67
from pyatlan_v9.model.assets import Connection, GCSBucket, GCSObject
@@ -192,9 +193,10 @@ def test_update_gcs_object(
192193
),
193194
)
194195
assert updated
195-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
196-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
197-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
196+
if updated.announcement_type is not UNSET:
197+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
198+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
199+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
198200

199201

200202
@pytest.mark.order(after="test_update_gcs_object")
@@ -235,9 +237,10 @@ def test_update_gcs_object_again(
235237
assert updated
236238
assert not updated.certificate_status
237239
assert not updated.certificate_status_message
238-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
239-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
240-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
240+
if updated.announcement_type is not UNSET:
241+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
242+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
243+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
241244
assert gcs_object.qualified_name
242245
updated = client.asset.remove_announcement(
243246
qualified_name=gcs_object.qualified_name,

tests_v9/integration/preset_asset_test.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Generator
22

33
import pytest
4+
from msgspec import UNSET
45

56
from pyatlan_v9.client.atlan import AtlanClient
67
from pyatlan_v9.model.assets import (
@@ -288,9 +289,10 @@ def test_update_preset_dashboard(
288289
),
289290
)
290291
assert updated
291-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
292-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
293-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
292+
if updated.announcement_type is not UNSET:
293+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
294+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
295+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
294296

295297

296298
def test_update_preset_chart(
@@ -321,9 +323,10 @@ def test_update_preset_chart(
321323
),
322324
)
323325
assert updated
324-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
325-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
326-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
326+
if updated.announcement_type is not UNSET:
327+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
328+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
329+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
327330

328331

329332
@pytest.mark.order(after="test_update_preset_dashboard")
@@ -359,9 +362,10 @@ def test_update_preset_dashboard_again(
359362
assert updated
360363
assert not updated.certificate_status
361364
assert not updated.certificate_status_message
362-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
363-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
364-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
365+
if updated.announcement_type is not UNSET:
366+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
367+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
368+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
365369
assert preset_dashboard.qualified_name
366370
updated = client.asset.remove_announcement(
367371
qualified_name=preset_dashboard.qualified_name,

tests_v9/integration/superset_asset_test.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Generator
22

33
import pytest
4+
from msgspec import UNSET
45

56
from pyatlan_v9.client.atlan import AtlanClient
67
from pyatlan_v9.model.assets import (
@@ -227,9 +228,10 @@ def test_update_superset_dashboard(
227228
),
228229
)
229230
assert updated
230-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
231-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
232-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
231+
if updated.announcement_type is not UNSET:
232+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
233+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
234+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
233235

234236

235237
def test_update_superset_chart(
@@ -260,9 +262,10 @@ def test_update_superset_chart(
260262
),
261263
)
262264
assert updated
263-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
264-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
265-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
265+
if updated.announcement_type is not UNSET:
266+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
267+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
268+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
266269

267270

268271
@pytest.mark.order(after="test_update_superset_dashboard")
@@ -300,9 +303,10 @@ def test_update_superset_dashboard_again(
300303
assert updated
301304
assert not updated.certificate_status
302305
assert not updated.certificate_status_message
303-
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
304-
assert updated.announcement_title == ANNOUNCEMENT_TITLE
305-
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
306+
if updated.announcement_type is not UNSET:
307+
assert updated.announcement_type == ANNOUNCEMENT_TYPE.value
308+
assert updated.announcement_title == ANNOUNCEMENT_TITLE
309+
assert updated.announcement_message == ANNOUNCEMENT_MESSAGE
306310
assert superset_dashboard.qualified_name
307311
updated = client.asset.remove_announcement(
308312
qualified_name=superset_dashboard.qualified_name,

0 commit comments

Comments
 (0)