@@ -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
195259def _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 :
0 commit comments