Skip to content

Commit 1132e18

Browse files
committed
[change] Remove duplicated dataclass/enum models, re-export from legacy
- search.py: delete 25 duplicated dataclass/ABC/Enum classes and ABC registration block; re-export from pyatlan.model.search. Keep only msgspec DSL/IndexSearchRequest/IndexSearchRequestMetadata + v9 helpers. - lineage.py: delete duplicated DirectedPair/LineageGraph; re-export from pyatlan.model.lineage. - audit.py: delete duplicated AuditActionType; re-export from pyatlan.model.audit. - pyatlan/model/search.py: TermAttributes/TextAttributes use plain str/bool/float instead of Pydantic StrictStr/StrictBool/StrictFloat. - pyatlan/validate.py: add _is_model_instance helper for cross-boundary Pydantic/msgspec isinstance checks. - pyatlan/client/common/asset.py: register msgspec.Struct in Pydantic ENCODERS_BY_TYPE for JSON serialization. - core.py: add to_dict() on BulkRequest for nested serialization. - entity.py: add semantic field to Entity base class. - asset.py: ref_by_guid/ref_by_qualified_name accept semantic param. - tests: update VALUES_BY_TYPE to plain types, use _is_model_instance for cross-boundary assertions, clean up test_client.py imports.
1 parent 1981878 commit 1132e18

12 files changed

Lines changed: 289 additions & 2055 deletions

File tree

pyatlan/client/common/asset.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def _ensure_type_filter_present(criteria: IndexSearchRequest) -> None:
112112
criteria
113113
and criteria.dsl
114114
and criteria.dsl.query
115-
and isinstance(criteria.dsl.query, Bool)
115+
and _is_model_instance(criteria.dsl.query, Bool)
116116
):
117117
return
118118

@@ -126,7 +126,8 @@ def _ensure_type_filter_present(criteria: IndexSearchRequest) -> None:
126126

127127
def needs_type_filter(clause: Optional[List]) -> bool:
128128
return not any(
129-
isinstance(f, (Term, Terms)) and f.field in (type_name_fields)
129+
(_is_model_instance(f, Term) or _is_model_instance(f, Terms))
130+
and f.field in (type_name_fields)
130131
for f in clause or []
131132
)
132133

pyatlan/model/search.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -50,41 +50,41 @@ def __new__(cls, value: str, attribute_type: type) -> "Attributes":
5050

5151
class TermAttributes(Attributes):
5252
CONNECTOR_NAME = ("connectorName", AtlanConnectorType)
53-
CATEGORIES = ("__categories", StrictStr)
53+
CATEGORIES = ("__categories", str)
5454
CREATE_TIME_AS_TIMESTAMP = ("__timestamp", datetime)
55-
CREATED_BY = ("__createdBy", StrictStr)
56-
GLOSSARY = ("__glossary", StrictStr)
57-
GUID = ("__guid", StrictStr)
58-
HAS_LINEAGE = ("__hasLineage", StrictBool)
59-
MEANINGS = ("__meanings", StrictStr)
60-
MODIFIED_BY = ("__modifiedBy", StrictStr)
61-
NAME = ("name.keyword", StrictStr)
62-
OWNER_GROUPS = ("ownerGroups", StrictStr)
63-
OWNER_USERS = ("ownerUsers", StrictStr)
64-
PARENT_CATEGORY = ("__parentCategory", StrictStr)
55+
CREATED_BY = ("__createdBy", str)
56+
GLOSSARY = ("__glossary", str)
57+
GUID = ("__guid", str)
58+
HAS_LINEAGE = ("__hasLineage", bool)
59+
MEANINGS = ("__meanings", str)
60+
MODIFIED_BY = ("__modifiedBy", str)
61+
NAME = ("name.keyword", str)
62+
OWNER_GROUPS = ("ownerGroups", str)
63+
OWNER_USERS = ("ownerUsers", str)
64+
PARENT_CATEGORY = ("__parentCategory", str)
6565
POPULARITY_SCORE = ("popularityScore", float)
66-
QUALIFIED_NAME = ("qualifiedName", StrictStr)
66+
QUALIFIED_NAME = ("qualifiedName", str)
6767
STATE = ("__state", Literal["ACTIVE", "DELETED", "PURGED"])
68-
SUPER_TYPE_NAMES = ("__superTypeNames.keyword", StrictStr)
69-
TYPE_NAME = ("__typeName.keyword", StrictStr)
68+
SUPER_TYPE_NAMES = ("__superTypeNames.keyword", str)
69+
TYPE_NAME = ("__typeName.keyword", str)
7070
UPDATE_TIME_AS_TIMESTAMP = ("__modificationTimestamp", datetime)
7171
CERTIFICATE_STATUS = ("certificateStatus", CertificateStatus)
7272

7373

7474
class TextAttributes(Attributes):
75-
CLASSIFICATION_NAMES = ("__classificationNames", StrictStr)
76-
CLASSIFICATIONS_TEXT = ("__classificationsText", StrictStr)
77-
CREATE_TIME_AS_DATE = ("__timestamp.date", StrictStr)
78-
DESCRIPTION = ("description", StrictStr)
79-
MEANINGS_TEXT = ("__meaningsText", StrictStr)
80-
NAME = ("name", StrictStr)
81-
QUALIFIED_NAME = ("qualifiedName.text", StrictStr)
82-
PROPAGATED_CLASSIFICATION_NAMES = ("__propagatedClassificationNames", StrictStr)
83-
PROPAGATED_TRAIT_NAMES = ("__propagatedTraitNames", StrictStr)
84-
SUPER_TYPE_NAMES = ("__superTypeNames", StrictStr)
85-
TRAIT_NAMES = ("__traitNames", StrictStr)
86-
UPDATE_TIME_AS_DATE = ("__modificationTimestamp.date", StrictStr)
87-
USER_DESCRIPTION = ("userDescription", StrictStr)
75+
CLASSIFICATION_NAMES = ("__classificationNames", str)
76+
CLASSIFICATIONS_TEXT = ("__classificationsText", str)
77+
CREATE_TIME_AS_DATE = ("__timestamp.date", str)
78+
DESCRIPTION = ("description", str)
79+
MEANINGS_TEXT = ("__meaningsText", str)
80+
NAME = ("name", str)
81+
QUALIFIED_NAME = ("qualifiedName.text", str)
82+
PROPAGATED_CLASSIFICATION_NAMES = ("__propagatedClassificationNames", str)
83+
PROPAGATED_TRAIT_NAMES = ("__propagatedTraitNames", str)
84+
SUPER_TYPE_NAMES = ("__superTypeNames", str)
85+
TRAIT_NAMES = ("__traitNames", str)
86+
UPDATE_TIME_AS_DATE = ("__modificationTimestamp.date", str)
87+
USER_DESCRIPTION = ("userDescription", str)
8888

8989

9090
def get_with_string(attribute: TermAttributes):

pyatlan/validate.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ class with the same name as *expected_type*.
8484
if expected_type.__name__ in spec_mro_names:
8585
return True
8686

87+
# Generic fallback: match by class name in MRO for any class
88+
# (handles dataclasses, plain classes, etc. with identical names
89+
# across legacy and v9 modules)
90+
value_mro_names = {cls.__name__ for cls in type(value).__mro__}
91+
if expected_type.__name__ in value_mro_names:
92+
return True
93+
8794
return False
8895

8996

pyatlan_v9/model/assets/asset.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import msgspec
1919
from msgspec import UNSET, UnsetType
2020

21+
from pyatlan_v9.model.assets.related_entity import SaveSemantic
2122
from pyatlan_v9.model.conversion_utils import (
2223
categorize_relationships,
2324
merge_relationships,
@@ -667,7 +668,9 @@ def from_json(json_data: Union[str, bytes], serde: Serde | None = None) -> "Asse
667668
# =========================================================================
668669

669670
@classmethod
670-
def ref_by_guid(cls, guid: str, semantic: str = "REPLACE") -> "Asset":
671+
def ref_by_guid(
672+
cls, guid: str, semantic: "SaveSemantic | str" = SaveSemantic.REPLACE
673+
) -> "Asset":
671674
"""
672675
Create a minimal reference to this asset type by its GUID.
673676
@@ -678,11 +681,13 @@ def ref_by_guid(cls, guid: str, semantic: str = "REPLACE") -> "Asset":
678681
Returns:
679682
Asset reference instance
680683
"""
681-
return cls(guid=guid, type_name=cls.__name__)
684+
if isinstance(semantic, str):
685+
semantic = SaveSemantic(semantic)
686+
return cls(guid=guid, type_name=cls.__name__, semantic=semantic)
682687

683688
@classmethod
684689
def ref_by_qualified_name(
685-
cls, qualified_name: str, semantic: str = "REPLACE"
690+
cls, qualified_name: str, semantic: "SaveSemantic | str" = SaveSemantic.REPLACE
686691
) -> "Asset":
687692
"""
688693
Create a minimal reference to this asset type by its qualifiedName.
@@ -694,7 +699,9 @@ def ref_by_qualified_name(
694699
Returns:
695700
Asset reference instance
696701
"""
697-
return cls(qualified_name=qualified_name, type_name=cls.__name__)
702+
if isinstance(semantic, str):
703+
semantic = SaveSemantic(semantic)
704+
return cls(qualified_name=qualified_name, type_name=cls.__name__, semantic=semantic)
698705

699706
# =========================================================================
700707
# Asset Mutation Convenience Methods

pyatlan_v9/model/assets/entity.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import msgspec
1919
from msgspec import UNSET, UnsetType
2020

21+
from pyatlan_v9.model.assets.related_entity import SaveSemantic
22+
2123

2224
class AtlasClassification(
2325
msgspec.Struct, kw_only=True, omit_defaults=True, rename="camel"
@@ -174,6 +176,11 @@ class Entity(msgspec.Struct, kw_only=True, omit_defaults=True, rename="camel"):
174176
home_id: Union[str, UnsetType] = UNSET
175177
"""Home identifier for distributed Atlas systems."""
176178

179+
semantic: Union[SaveSemantic, None, UnsetType] = UNSET
180+
"""Save semantic for relationship operations (REPLACE, APPEND, REMOVE).
181+
Not serialized to JSON — used internally to control how relationship
182+
attributes are categorized during bulk save operations."""
183+
177184
# =========================================================================
178185
# Compatibility Methods (legacy API surface)
179186
# =========================================================================

pyatlan_v9/model/audit.py

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

55
import json as json_lib
66
from datetime import datetime
7-
from enum import Enum
87
from typing import Any, Dict, Generator, Iterable, List, Optional, Set, Union
98

109
import msgspec
@@ -18,6 +17,11 @@
1817
from pyatlan_v9.model.aggregation import Aggregation
1918
from pyatlan_v9.model.search import DSL, Bool, Query, Range, SortItem, Term
2019

20+
# ---------------------------------------------------------------------------
21+
# Re-export plain Enum from legacy — no migration needed.
22+
# ---------------------------------------------------------------------------
23+
from pyatlan.model.audit import AuditActionType # noqa: F401
24+
2125
TOTAL_COUNT = "totalCount"
2226

2327
ENTITY_AUDITS = "entityAudits"
@@ -29,24 +33,6 @@
2933
LATEST_FIRST = [SortItem("created", order=SortOrder.DESCENDING)]
3034

3135

32-
class AuditActionType(str, Enum):
33-
ENTITY_CREATE = "ENTITY_CREATE"
34-
ENTITY_UPDATE = "ENTITY_UPDATE"
35-
ENTITY_DELETE = "ENTITY_DELETE"
36-
CUSTOM_METADATA_UPDATE = "BUSINESS_ATTRIBUTE_UPDATE"
37-
ATLAN_TAG_ADD = "CLASSIFICATION_ADD"
38-
PROPAGATED_ATLAN_TAG_ADD = "PROPAGATED_CLASSIFICATION_ADD"
39-
ATLAN_TAG_DELETE = "CLASSIFICATION_DELETE"
40-
PROPAGATED_ATLAN_TAG_DELETE = "PROPAGATED_CLASSIFICATION_DELETE"
41-
ENTITY_IMPORT_CREATE = "ENTITY_IMPORT_CREATE"
42-
ENTITY_IMPORT_UPDATE = "ENTITY_IMPORT_UPDATE"
43-
ENTITY_IMPORT_DELETE = "ENTITY_IMPORT_DELETE"
44-
ATLAN_TAG_UPDATE = "CLASSIFICATION_UPDATE"
45-
PROPAGATED_ATLAN_TAG_UPDATE = "PROPAGATED_CLASSIFICATION_UPDATE"
46-
TERM_ADD = "TERM_ADD"
47-
TERM_DELETE = "TERM_DELETE"
48-
49-
5036
class AuditSearchRequest(msgspec.Struct, kw_only=True):
5137
"""Class from which to configure a search against Atlan's activity log."""
5238

pyatlan_v9/model/conversion_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ def categorize_relationships(
4343

4444
# Handle list of related entities
4545
if isinstance(value, list):
46+
if len(value) == 0:
47+
# Empty list means "replace with nothing" (clear the relationship)
48+
replace_kwargs[field_name] = []
4649
for item in value:
4750
semantic = getattr(item, "semantic", UNSET)
4851
if semantic is UNSET or semantic == SaveSemantic.REPLACE:

pyatlan_v9/model/core.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -359,10 +359,29 @@ class BulkRequest(msgspec.Struct, kw_only=True, rename="camel"):
359359
"""
360360
Wrapper for bulk asset API requests.
361361
362-
Note: The original Pydantic version included a @validator that processed
363-
relationship attributes (append/remove semantics). In v9 that processing
364-
logic is handled separately outside this struct.
362+
In v9, relationship categorization (replace/append/remove semantics) is
363+
handled by each entity's ``to_json(nested=True)`` conversion, which calls
364+
``categorize_relationships()`` under the hood.
365365
"""
366366

367367
entities: list[Any]
368368
"""List of asset entities to send to the API in bulk."""
369+
370+
def to_dict(self) -> dict:
371+
"""
372+
Convert to a dict in the API nested format.
373+
374+
Each entity is converted to its nested representation (with
375+
``attributes``, ``relationshipAttributes``,
376+
``appendRelationshipAttributes``, ``removeRelationshipAttributes``).
377+
378+
Returns:
379+
Dict suitable for JSON serialization and API submission.
380+
"""
381+
entity_dicts = []
382+
for entity in self.entities:
383+
if hasattr(entity, "to_json"):
384+
entity_dicts.append(json.loads(entity.to_json(nested=True)))
385+
else:
386+
entity_dicts.append(msgspec.to_builtins(entity))
387+
return {"entities": entity_dicts}

0 commit comments

Comments
 (0)