Skip to content

Commit dfb50b8

Browse files
committed
[generator + tests] Updated generator scripts + added unit tests
1 parent 00ad1c5 commit dfb50b8

File tree

52 files changed

+4499
-155
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4499
-155
lines changed

pyatlan/generator/class_generator.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import networkx as nx
1919
from jinja2 import Environment, PackageLoader
2020

21-
from pyatlan.model.typedef import EntityDef, EnumDef, TypeDefResponse
22-
from pyatlan.model.utils import to_snake_case
21+
from pyatlan.model.typedef import EntityDef, EnumDef, RelationshipDef, TypeDefResponse
22+
from pyatlan.model.utils import to_python_class_name, to_snake_case
2323

2424
REFERENCEABLE = "Referenceable"
2525
TYPE_DEF_FILE = Path(os.getenv("TMPDIR", "/tmp")) / "typedefs.json"
@@ -80,6 +80,7 @@
8080
# across Python versions (e.g: 3.8 and 3.9).
8181
PARENT = Path(__file__).resolve().parent
8282
ASSETS_DIR = PARENT.parent / "model" / "assets"
83+
ASSETS_RELATIONS_DIR = PARENT.parent / "model" / "assets" / "relations"
8384
CORE_ASSETS_DIR = PARENT.parent / "model" / "assets" / "core"
8485
MODEL_DIR = PARENT.parent / "model"
8586
DOCS_DIR = PARENT.parent / "documentation"
@@ -450,6 +451,29 @@ def create_modules(cls):
450451
cls._CORE_ASSETS.add(related_asset.super_class)
451452

452453

454+
class RelationshipDefInfo:
455+
module_name: str
456+
relationship_def_infos: List["RelationshipDefInfo"] = []
457+
458+
def __init__(self, name: str, relationship_def: RelationshipDef):
459+
self.module_name = to_snake_case(name)
460+
self.relationship_def = relationship_def
461+
462+
@classmethod
463+
def create(cls, relationship_defs: List[RelationshipDef]):
464+
for rel_def in relationship_defs:
465+
# Only pick `atlas_core` enums, not user-created ones.
466+
if rel_def.attribute_defs:
467+
cls.relationship_def_infos.append(
468+
RelationshipDefInfo(
469+
name=to_snake_case(rel_def.name), relationship_def=rel_def
470+
)
471+
)
472+
# if rel_def.name == "UserDefRelationship":
473+
# import ipdb; ipdb.set_trace()
474+
# print(to_python_class_name(rel_def.name))
475+
476+
453477
class AttributeType(Enum):
454478
PRIMITIVE = "PRIMITIVE"
455479
ENUM = "ENUM"
@@ -664,6 +688,7 @@ def __init__(self) -> None:
664688
)
665689
self.environment.filters["to_snake_case"] = to_snake_case
666690
self.environment.filters["get_type"] = get_type
691+
self.environment.filters["to_cls_name"] = to_python_class_name
667692
self.environment.filters["get_search_type"] = get_search_type
668693
self.environment.filters["get_mapped_type"] = get_mapped_type
669694
self.environment.filters["get_class_var_for_attr"] = get_class_var_for_attr
@@ -719,6 +744,21 @@ def render_module(self, asset_info: AssetInfo, enum_defs: List["EnumDefInfo"]):
719744
with (ASSETS_DIR / f"{asset_info.module_name}.py").open("w") as script:
720745
script.write(content)
721746

747+
def render_custom_relationship_module(
748+
self, relationship_def_info: RelationshipDefInfo
749+
):
750+
template = self.environment.get_template("custom_relationship.jinja2")
751+
content = template.render(
752+
{
753+
"relationship_info": relationship_info.relationship_def,
754+
"templates_path": TEMPLATES_DIR.absolute().as_posix(),
755+
}
756+
)
757+
with (ASSETS_RELATIONS_DIR / f"{relationship_def_info.module_name}.py").open(
758+
"w"
759+
) as script:
760+
script.write(content)
761+
722762
def render_core_module(self, asset_info: AssetInfo, enum_defs: List["EnumDefInfo"]):
723763
template = self.environment.get_template("module.jinja2")
724764
content = template.render(
@@ -769,6 +809,22 @@ def render_mypy_init(self, assets: List[AssetInfo]):
769809
with init_path.open("w") as script:
770810
script.write(content)
771811

812+
def render_relations_init(self, relation_def_infos: List[RelationshipDefInfo]):
813+
template = self.environment.get_template("relations_init.jinja2")
814+
content = template.render({"relation_def_infos": relation_def_infos})
815+
816+
init_path = ASSETS_RELATIONS_DIR / "__init__.py"
817+
with init_path.open("w") as script:
818+
script.write(content)
819+
820+
def render_relations_mypy_init(self, relation_def_infos: List[RelationshipDefInfo]):
821+
template = self.environment.get_template("relations_mypy_init.jinja2")
822+
content = template.render({"relation_def_infos": relation_def_infos})
823+
824+
init_path = ASSETS_RELATIONS_DIR / "__init__.pyi"
825+
with init_path.open("w") as script:
826+
script.write(content)
827+
772828
def render_structs(self, struct_defs):
773829
template = self.environment.get_template("structs.jinja2")
774830
content = template.render({"struct_defs": struct_defs})
@@ -933,8 +989,10 @@ def filter_attributes_of_custom_entity_type():
933989
filter_attributes_of_custom_entity_type()
934990
AssetInfo.sub_type_names_to_ignore = type_defs.custom_entity_def_names
935991
AssetInfo.set_entity_defs(type_defs.reserved_entity_defs)
992+
RelationshipDefInfo.create(type_defs.relationship_defs)
936993
AssetInfo.update_all_circular_dependencies()
937994
AssetInfo.create_modules()
995+
938996
for file in (ASSETS_DIR).glob("*.py"):
939997
file.unlink()
940998
for file in (CORE_ASSETS_DIR).glob("*.py"):
@@ -944,14 +1002,28 @@ def filter_attributes_of_custom_entity_type():
9441002
file.unlink()
9451003
generator = Generator()
9461004
EnumDefInfo.create(type_defs.enum_defs)
1005+
9471006
for asset_info in ModuleInfo.assets.values():
9481007
if asset_info.is_core_asset or asset_info.name in asset_info._CORE_ASSETS:
9491008
generator.render_core_module(asset_info, EnumDefInfo.enum_def_info)
9501009
else:
9511010
generator.render_module(asset_info, EnumDefInfo.enum_def_info)
1011+
1012+
for file in (ASSETS_RELATIONS_DIR).glob("*.py"):
1013+
# Remove all files in the core assets
1014+
# directory except `relationship_attributes.py`
1015+
if file.name == "relationship_attributes.py":
1016+
continue
1017+
file.unlink()
1018+
1019+
for relationship_info in RelationshipDefInfo.relationship_def_infos:
1020+
generator.render_custom_relationship_module(relationship_info)
1021+
9521022
generator.render_init(ModuleInfo.assets.values()) # type: ignore
9531023
generator.render_core_init(ModuleInfo.assets.values()) # type: ignore
9541024
generator.render_mypy_init(ModuleInfo.assets.values()) # type: ignore
1025+
generator.render_relations_init(RelationshipDefInfo.relationship_def_infos) # type: ignore
1026+
generator.render_relations_mypy_init(RelationshipDefInfo.relationship_def_infos) # type: ignore
9551027
generator.render_structs(type_defs.struct_defs)
9561028
generator.render_enums(EnumDefInfo.enum_def_info)
9571029
generator.render_docs_struct_snippets(type_defs.struct_defs)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Optional
4+
5+
from pydantic.v1 import Field, validator
6+
7+
from pyatlan.model.assets import Asset
8+
from pyatlan.model.assets.relations import RelationshipAttributes
9+
from pyatlan.model.core import AtlanObject
10+
from pyatlan.model.enums import SaveSemantic, AtlasGlossaryTermRelationshipStatus
11+
12+
13+
class {{ relationship_info.name | to_cls_name }}(RelationshipAttributes):
14+
type_name: str = Field(
15+
allow_mutation=False,
16+
default="{{ relationship_info.name }}",
17+
description="{{ relationship_info.description }}",
18+
)
19+
attributes: {{ relationship_info.name | to_cls_name }}.Attributes = Field(
20+
default_factory=lambda: {{ relationship_info.name | to_cls_name }}.Attributes(),
21+
description="Map of attributes in the instance and their values",
22+
)
23+
24+
class Attributes(AtlanObject):
25+
{% for attribute_def in relationship_info.attribute_defs -%}
26+
{{ attribute_def["name"] | to_snake_case }}: Optional[{{ attribute_def["typeName"] | get_type }}] = Field(
27+
default=None,
28+
description="{{ attribute_def["description"] }}",
29+
)
30+
{% endfor %}
31+
32+
def __init__(__pydantic_self__, **data: Any) -> None:
33+
if "attributes" not in data:
34+
data = {"attributes": data}
35+
super().__init__(**data)
36+
__pydantic_self__.__fields_set__.update(["attributes", "type_name"])
37+
38+
class {{ relationship_info.end_def1["name"] | to_cls_name }}(Asset):
39+
type_name: str = Field(
40+
default="{{ relationship_info.name }}",
41+
description="{{ relationship_info.end_def1["description"] or 'Name of the relationship type that defines the relationship.'}}",
42+
)
43+
relationship_type: str = Field(
44+
default="{{ relationship_info.name }}",
45+
description="Fixed typeName for {{ relationship_info.name }}.",
46+
)
47+
relationship_attributes: {{ relationship_info.name | to_cls_name }} = Field(
48+
default=None,
49+
description="Attributes of the {{ relationship_info.name }}.",
50+
)
51+
52+
@validator("type_name")
53+
def validate_type_name(cls, v):
54+
return v
55+
56+
def __init__(__pydantic_self__, **data: Any) -> None:
57+
super().__init__(**data)
58+
__pydantic_self__.__fields_set__.update(["type_name", "relationship_type"])
59+
60+
class {{ relationship_info.end_def2["name"] | to_cls_name }}(Asset):
61+
type_name: str = Field(
62+
default="{{ relationship_info.name }}",
63+
description="{{ relationship_info.end_def2["description"] or 'Name of the relationship type that defines the relationship.'}}",
64+
)
65+
relationship_type: str = Field(
66+
default="{{ relationship_info.name }}",
67+
description="Fixed typeName for {{ relationship_info.name }}.",
68+
)
69+
relationship_attributes: {{ relationship_info.name | to_cls_name }} = Field(
70+
default=None,
71+
description="Attributes of the {{ relationship_info.name }}.",
72+
)
73+
74+
@validator("type_name")
75+
def validate_type_name(cls, v):
76+
return v
77+
78+
def __init__(__pydantic_self__, **data: Any) -> None:
79+
super().__init__(**data)
80+
__pydantic_self__.__fields_set__.update(["type_name", "relationship_type"])
81+
82+
def {{ relationship_info.end_def1["name"] | to_snake_case }}(
83+
self, related: Asset, semantic: SaveSemantic = SaveSemantic.REPLACE
84+
) -> {{ relationship_info.name | to_cls_name }}.{{ relationship_info.end_def1["name"] | to_cls_name }}:
85+
if related.guid:
86+
return {{ relationship_info.name | to_cls_name }}.{{ relationship_info.end_def1["name"] | to_cls_name }}._create_ref(
87+
type_name=related.type_name,
88+
guid=related.guid,
89+
semantic=semantic,
90+
relationship_attributes=self,
91+
)
92+
93+
# If the related asset does not have a GUID, we use qualifiedName
94+
return {{ relationship_info.name | to_cls_name }}.{{ relationship_info.end_def1["name"] | to_cls_name }}._create_ref(
95+
type_name=related.type_name,
96+
unique_attributes={"qualifiedName": related.qualified_name},
97+
semantic=semantic,
98+
relationship_attributes=self,
99+
)
100+
101+
def {{ relationship_info.end_def2["name"] | to_snake_case }}(
102+
self, related: Asset, semantic: SaveSemantic = SaveSemantic.REPLACE
103+
) -> {{ relationship_info.name | to_cls_name }}.{{ relationship_info.end_def2["name"] | to_cls_name }}:
104+
if related.guid:
105+
return {{ relationship_info.name | to_cls_name }}.{{ relationship_info.end_def2["name"] | to_cls_name }}._create_ref(
106+
type_name=related.type_name,
107+
guid=related.guid,
108+
semantic=semantic,
109+
relationship_attributes=self,
110+
)
111+
112+
# If the related asset does not have a GUID, we use qualifiedName
113+
return {{ relationship_info.name | to_cls_name }}.{{ relationship_info.end_def2["name"] | to_cls_name }}._create_ref(
114+
type_name=related.type_name,
115+
unique_attributes={"qualifiedName": related.qualified_name},
116+
semantic=semantic,
117+
relationship_attributes=self,
118+
)
119+
120+
121+
{{ relationship_info.name | to_cls_name }}.{{ relationship_info.end_def1["name"] | to_cls_name }}.update_forward_refs()
122+
{{ relationship_info.name | to_cls_name }}.{{ relationship_info.end_def2["name"] | to_cls_name }}.update_forward_refs()
123+
{{ relationship_info.name | to_cls_name }}.update_forward_refs()

pyatlan/generator/templates/imports.jinja2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,4 @@ from pyatlan.model.data_mesh import DataProductsAssetsDSL
127127
from pyatlan.model.contract import DataContractSpec
128128
from pyatlan.model.lineage_ref import LineageRef
129129
from pyatlan.model.utils import construct_object_key
130+
from pyatlan.model.assets.relations import RelationshipAttributes

pyatlan/generator/templates/referenceable_attributes.jinja2

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
{%- set type = attribute_def.typeName | get_type %}
1010
{{attribute_def.name | to_snake_case }}: {% if attribute_def.isOptional %}Optional[{% endif %}{{type}}{% if attribute_def.isOptional %}]{% endif %} = Field({% if attribute_def.isOptional %}default=None,{% endif %} description='') # relationship
1111
{%- endfor %}
12+
relationship_attributes: Optional[RelationshipAttributes] = Field(
13+
default=None,
14+
description="Map of relationships for the entity. The specific keys of this map will vary by type, "
15+
"so are described in the sub-types of this schema.",
16+
)
1217

1318
def validate_required(self):
1419
pass
@@ -122,10 +127,10 @@
122127
)
123128
is_incomplete: Optional[bool] = Field(default=None, description="", example=True)
124129
labels: Optional[List[str]] = Field(default=None, description='Arbitrary textual labels for the asset.')
125-
relationship_attributes: Optional[Dict[str, Any]] = Field(
130+
relationship_attributes: Optional[RelationshipAttributes] = Field(
126131
default=None,
127-
description='Map of relationships for the entity. The specific keys of this map will vary by type, '
128-
'so are described in the sub-types of this schema.',
132+
description="Map of relationships for the entity. The specific keys of this map will vary by type, "
133+
"so are described in the sub-types of this schema.",
129134
)
130135
status: Optional[EntityStatus] = Field(
131136
default=None,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2025 Atlan Pte. Ltd.
2+
# isort: skip_file
3+
import lazy_loader as lazy
4+
5+
__PYATLAN_ASSET_RELATIONS__ = {
6+
"relationship_attributes": ["RelationshipAttributes"],
7+
{% for relation in relation_def_infos -%}
8+
"{{ relation.relationship_def.name | to_snake_case }}": ["{{ relation.relationship_def.name | to_cls_name}}"],
9+
{% endfor %}
10+
}
11+
12+
lazy_loader = lazy.attach(__name__, submod_attrs=__PYATLAN_ASSET_RELATIONS__)
13+
__getattr__, __dir__, __all__ = lazy_loader
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2025 Atlan Pte. Ltd.
2+
3+
__all__ = [
4+
"RelationshipAttributes",
5+
{% for relation in relation_def_infos -%}
6+
"{{ relation.relationship_def.name | to_cls_name}}",
7+
{% endfor %}
8+
]
9+
10+
from .relationship_attributes import RelationshipAttributes
11+
{% for relation in relation_def_infos -%}
12+
from .{{ relation.relationship_def.name | to_snake_case }} import {{ relation.relationship_def.name | to_cls_name }}
13+
{% endfor %}

pyatlan/generator/templates/structs.jinja2

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ from pyatlan.model.enums import (
1313
BadgeComparisonOperator,
1414
BadgeConditionColor,
1515
SourceCostUnitType,
16-
alpha_DQRuleThresholdUnit,
1716
FormFieldDimension,
1817
FormFieldType
1918
)

0 commit comments

Comments
 (0)