Skip to content

Commit ad43abb

Browse files
authored
Merge branch 'main' into APP-7824
2 parents 07f5f06 + 7af2190 commit ad43abb

26 files changed

+1137
-61
lines changed

pyatlan/generator/templates/imports.jinja2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ from pyatlan.model.structs import (
120120
from pyatlan.utils import (
121121
init_guid,
122122
next_id,
123+
to_camel_case,
123124
validate_required_fields,
124125
validate_single_required_field,
125126
get_parent_qualified_name,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
@overload
3+
@classmethod
4+
def creator(
5+
cls,
6+
*,
7+
name: str,
8+
ai_application_version: str,
9+
ai_application_development_stage: AIApplicationDevelopmentStage,
10+
) -> AIApplication: ...
11+
12+
@overload
13+
@classmethod
14+
def creator(
15+
cls,
16+
*,
17+
name: str,
18+
ai_application_version: str,
19+
ai_application_development_stage: AIApplicationDevelopmentStage,
20+
owner_groups: Set[str],
21+
owner_users: Set[str],
22+
models: List[AIModel],
23+
) -> AIApplication: ...
24+
25+
@classmethod
26+
@init_guid
27+
def creator(
28+
cls,
29+
*,
30+
name: str,
31+
ai_application_version: str,
32+
ai_application_development_stage: AIApplicationDevelopmentStage,
33+
owner_groups: Optional[Set[str]] = None,
34+
owner_users: Optional[Set[str]] = None,
35+
models: Optional[List[AIModel]] = None,
36+
) -> AIApplication:
37+
validate_required_fields(
38+
["name", "ai_application_version", "ai_application_development_stage"],
39+
[name, ai_application_version, ai_application_development_stage],
40+
)
41+
attributes = AIApplication.Attributes.creator(
42+
name=name,
43+
ai_application_version=ai_application_version,
44+
ai_application_development_stage=ai_application_development_stage,
45+
owner_groups=owner_groups,
46+
owner_users=owner_users,
47+
models=models,
48+
)
49+
return cls(attributes=attributes)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
2+
@overload
3+
@classmethod
4+
def creator(
5+
cls,
6+
*,
7+
name: str,
8+
ai_model_status: AIModelStatus,
9+
) -> AIModel: ...
10+
11+
@overload
12+
@classmethod
13+
def creator(
14+
cls,
15+
*,
16+
name: str,
17+
ai_model_status: AIModelStatus,
18+
owner_groups: Set[str],
19+
owner_users: Set[str],
20+
ai_model_version: str,
21+
) -> AIModel: ...
22+
23+
@classmethod
24+
@init_guid
25+
def creator(
26+
cls,
27+
*,
28+
name: str,
29+
ai_model_status: AIModelStatus,
30+
owner_groups: Optional[Set[str]] = None,
31+
owner_users: Optional[Set[str]] = None,
32+
ai_model_version: Optional[str] = None,
33+
) -> AIModel:
34+
validate_required_fields(
35+
["name", "ai_model_status"],
36+
[name, ai_model_status],
37+
)
38+
attributes = AIModel.Attributes.creator(
39+
name=name,
40+
ai_model_status=ai_model_status,
41+
owner_groups=owner_groups,
42+
owner_users=owner_users,
43+
ai_model_version=ai_model_version,
44+
)
45+
return cls(attributes=attributes)
46+
47+
@classmethod
48+
def processes_creator(
49+
cls,
50+
ai_model: AIModel,
51+
dataset_dict: Dict[AIDatasetType, list],
52+
) -> List[Process]:
53+
"""
54+
Creates a list of Process objects representing the relationships between an AI model and its datasets.
55+
56+
:param ai_model: the AI model for which to create processes
57+
:param dataset_dict: dictionary mapping AI dataset types to lists of assets
58+
:returns: list of Process objects representing the AI model's data lineage
59+
:raises ValueError: when the AI model is missing required attributes (guid or name)
60+
"""
61+
if not ai_model.guid or not ai_model.name:
62+
raise ValueError("AI model must have both guid and name attributes")
63+
process_list = []
64+
for key, value_list in dataset_dict.items():
65+
for value in value_list:
66+
asset_type = Asset._convert_to_real_type_(value)
67+
if key == AIDatasetType.OUTPUT:
68+
process_name = f"{ai_model.name} -> {value.name}"
69+
process_created = Process.creator(
70+
name=process_name,
71+
connection_qualified_name="default/ai/dataset",
72+
inputs=[AIModel.ref_by_guid(guid=ai_model.guid)],
73+
outputs=[asset_type.ref_by_guid(guid=value.guid)], # type: ignore
74+
extra_hash_params={key.value},
75+
)
76+
process_created.ai_dataset_type = key
77+
else:
78+
process_name = f"{value.name} -> {ai_model.name}"
79+
process_created = Process.creator(
80+
name=process_name,
81+
connection_qualified_name="default/ai/dataset",
82+
inputs=[asset_type.ref_by_guid(guid=value.guid)], # type: ignore
83+
outputs=[AIModel.ref_by_guid(guid=ai_model.guid)],
84+
extra_hash_params={key.value},
85+
)
86+
process_created.ai_dataset_type = key
87+
process_list.append(process_created)
88+
89+
return process_list
90+
91+
@classmethod
92+
def processes_batch_save(cls, client, process_list: List[Process]) -> List:
93+
"""
94+
Saves a list of Process objects to Atlan in batches to optimize performance.
95+
We save the processes in batches of 20.
96+
97+
:param client: Atlan client instance for making API calls
98+
:param process_list: list of Process objects to save
99+
:returns: list of API responses from each batch save operation
100+
"""
101+
batch_size = 20
102+
total_processes = len(process_list)
103+
responses = []
104+
105+
for i in range(0, total_processes, batch_size):
106+
batch = process_list[i : i + batch_size]
107+
response = client.asset.save(batch)
108+
responses.append(response)
109+
110+
return responses

pyatlan/generator/templates/methods/asset/process.jinja2

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
outputs: List["Catalog"],
1010
process_id: Optional[str] = None,
1111
parent: Optional[Process] = None,
12+
extra_hash_params: Optional[Set[str]] = None,
1213
) -> Process:
1314
return Process(
1415
attributes=Process.Attributes.create(
@@ -18,6 +19,7 @@
1819
inputs=inputs,
1920
outputs=outputs,
2021
parent=parent,
22+
extra_hash_params=extra_hash_params,
2123
)
2224
)
2325

@@ -31,6 +33,7 @@
3133
outputs: List["Catalog"],
3234
process_id: Optional[str] = None,
3335
parent: Optional[Process] = None,
36+
extra_hash_params: Optional[Set[str]] = None,
3437
) -> Process:
3538
warn(
3639
(
@@ -47,4 +50,5 @@
4750
outputs=outputs,
4851
process_id=process_id,
4952
parent=parent,
53+
extra_hash_params=extra_hash_params,
5054
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
@classmethod
3+
@init_guid
4+
def creator(
5+
cls,
6+
*,
7+
name: str,
8+
ai_application_version: str,
9+
ai_application_development_stage: AIApplicationDevelopmentStage,
10+
owner_groups: Optional[Set[str]] = None,
11+
owner_users: Optional[Set[str]] = None,
12+
models: Optional[List[AIModel]] = None,
13+
) -> AIApplication.Attributes:
14+
validate_required_fields(
15+
["name", "ai_application_version", "ai_application_development_stage"],
16+
[name, ai_application_version, ai_application_development_stage],
17+
)
18+
owner_groups = owner_groups or set()
19+
owner_users = owner_users or set()
20+
models = models or []
21+
name_camel_case = to_camel_case(name)
22+
return AIApplication.Attributes(
23+
name=name,
24+
qualified_name=f"default/ai/aiapplication/{name_camel_case}",
25+
connector_name=AtlanConnectorType.AI.value,
26+
ai_application_version=ai_application_version,
27+
ai_application_development_stage=ai_application_development_stage,
28+
owner_groups=owner_groups,
29+
owner_users=owner_users,
30+
models=models,
31+
certificate_status=CertificateStatus.DRAFT,
32+
asset_cover_image="/assets/default-product-cover-DeQonY47.webp",
33+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
@classmethod
3+
@init_guid
4+
def creator(
5+
cls,
6+
*,
7+
name: str,
8+
ai_model_status: AIModelStatus,
9+
owner_groups: Optional[Set[str]] = None,
10+
owner_users: Optional[Set[str]] = None,
11+
ai_model_version: Optional[str] = None,
12+
) -> AIModel.Attributes:
13+
validate_required_fields(
14+
["name", "ai_model_status"],
15+
[name, ai_model_status],
16+
)
17+
owner_groups = owner_groups or set()
18+
owner_users = owner_users or set()
19+
name_camel_case = to_camel_case(name)
20+
return AIModel.Attributes(
21+
name=name,
22+
qualified_name=f"default/ai/aiapplication/{name_camel_case}",
23+
connector_name=AtlanConnectorType.AI.value,
24+
ai_model_status=ai_model_status,
25+
ai_model_version=ai_model_version,
26+
owner_groups=owner_groups,
27+
owner_users=owner_users,
28+
asset_cover_image="/assets/default-product-cover-DeQonY47.webp",
29+
)

pyatlan/generator/templates/methods/attribute/process.jinja2

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
outputs: List["Catalog"],
88
parent: Optional["Process"] = None,
99
process_id: Optional[str] = None,
10+
extra_hash_params: Optional[Set[str]] = None,
1011
) -> str:
1112
def append_relationship(output: StringIO, relationship: Asset):
1213
if relationship.guid:
@@ -20,6 +21,7 @@
2021
["name", "connection_qualified_name", "inputs", "outputs"],
2122
[name, connection_qualified_name, inputs, outputs],
2223
)
24+
extra_hash_params = extra_hash_params or set()
2325
if process_id and process_id.strip():
2426
return f"{connection_qualified_name}/{process_id}"
2527
buffer = StringIO()
@@ -29,6 +31,11 @@
2931
append_relationship(buffer, parent)
3032
append_relationships(buffer, inputs)
3133
append_relationships(buffer, outputs)
34+
# Handles edge case where identical name, connection, input, and output caused hash collisions,
35+
# resulting in duplicate qualified names and backend skipping process creation.
36+
if extra_hash_params:
37+
for param in extra_hash_params:
38+
buffer.write(param)
3239
ret_value = hashlib.md5( # noqa: S303, S324
3340
buffer.getvalue().encode()
3441
).hexdigest()
@@ -45,6 +52,7 @@
4552
outputs: List["Catalog"],
4653
process_id: Optional[str] = None,
4754
parent: Optional[Process] = None,
55+
extra_hash_params: Optional[Set[str]] = None,
4856
) -> Process.Attributes:
4957
qualified_name = Process.Attributes.generate_qualified_name(
5058
name=name,
@@ -53,6 +61,7 @@
5361
inputs=inputs,
5462
outputs=outputs,
5563
parent=parent,
64+
extra_hash_params=extra_hash_params,
5665
)
5766
connector_name = connection_qualified_name.split("/")[1]
5867
return Process.Attributes(

pyatlan/generator/templates/module.jinja2

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ from pyatlan.model.enums import (
2020
from .asset import SelfAsset
2121
{% endif %}
2222

23+
{% if asset_info.name == 'AIModel' %}
24+
from .asset import Asset
25+
from .process import Process
26+
{% endif %}
27+
2328
{% set entity_def = asset_info.entity_def %}
2429

2530
{% set file_name = 'methods/imports/' + entity_def.name | to_snake_case + '.jinja2' %}

pyatlan/model/assets/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@
8888
"SnowflakeStage",
8989
"DatabricksUnityCatalogTag",
9090
"SnowflakeStream",
91-
"Database",
9291
"CalculationView",
92+
"Database",
9393
"Procedure",
9494
"SnowflakeTag",
9595
"MatillionGroup",

pyatlan/model/assets/__init__.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ __all__ = [
8585
"SnowflakeStage",
8686
"DatabricksUnityCatalogTag",
8787
"SnowflakeStream",
88-
"Database",
8988
"CalculationView",
89+
"Database",
9090
"Procedure",
9191
"SnowflakeTag",
9292
"MatillionGroup",

0 commit comments

Comments
 (0)