Skip to content

Commit 7ab85a2

Browse files
authored
APP-8028: Fix glossary, persona, purpose tests (using custom retries) (#695)
1 parent 3f7d250 commit 7ab85a2

File tree

5 files changed

+175
-29
lines changed

5 files changed

+175
-29
lines changed

pyatlan/client/asset.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,6 +1800,17 @@ def _search_for_asset_with_name(
18001800
attributes: Optional[List[StrictStr]],
18011801
allow_multiple: bool = False,
18021802
) -> List[A]:
1803+
"""
1804+
Search for assets by name.
1805+
1806+
:param query: the search query to execute
1807+
:param name: name of the asset being searched
1808+
:param asset_type: type of asset to find
1809+
:param attributes: attributes to retrieve
1810+
:param allow_multiple: whether to allow multiple results
1811+
:returns: list of found assets
1812+
:raises: ErrorCode.ASSET_NOT_FOUND_BY_NAME if no assets found
1813+
"""
18031814
dsl = DSL(query=query)
18041815
search_request = IndexSearchRequest(
18051816
dsl=dsl, attributes=attributes, relation_attributes=["name"]

tests/integration/glossary_test.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
from pyatlan.model.fluent_search import CompoundQuery, FluentSearch
1919
from pyatlan.model.search import DSL, IndexSearchRequest
2020
from tests.integration.client import TestId, delete_asset
21+
from tests.integration.utils import (
22+
assert_fluent_search_count_with_retry,
23+
assert_search_count_with_retry,
24+
)
2125

2226
LOGGER = logging.getLogger(__name__)
2327

@@ -484,9 +488,8 @@ def test_compound_queries(
484488
.where(AtlasGlossaryTerm.ANCHOR.eq(glossary.qualified_name))
485489
).to_query()
486490
request = IndexSearchRequest(dsl=DSL(query=cq))
487-
response = client.asset.search(request)
488-
assert response
489-
assert response.count == 4
491+
# Use centralized retry utility for eventual consistency
492+
assert_search_count_with_retry(client, request, expected_count=4)
490493
assert glossary.qualified_name
491494
assert term2.name
492495

@@ -499,9 +502,8 @@ def test_compound_queries(
499502
.where_not(AtlasGlossaryTerm.NAME.eq(term2.name))
500503
).to_query()
501504
request = IndexSearchRequest(dsl=DSL(query=cq))
502-
response = client.asset.search(request)
503-
assert response
504-
assert response.count == 3
505+
# Use centralized retry utility for eventual consistency
506+
assert_search_count_with_retry(client, request, expected_count=3)
505507

506508

507509
def test_fluent_search(
@@ -524,7 +526,8 @@ def test_fluent_search(
524526
.include_on_relations(AtlasGlossary.NAME)
525527
)
526528

527-
assert terms.count(client) == 4
529+
# Use centralized retry utility to handle search index eventual consistency
530+
assert_fluent_search_count_with_retry(terms, client, expected_count=4)
528531

529532
guids_chained = []
530533
g_sorted = []

tests/integration/persona_test.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# Copyright 2022 Atlan Pte. Ltd.
3-
import time
43
from typing import Generator
54

65
import pytest
@@ -18,6 +17,7 @@
1817
from tests.integration.client import TestId, delete_asset
1918
from tests.integration.connection_test import create_connection
2019
from tests.integration.glossary_test import create_glossary
20+
from tests.integration.utils import find_personas_by_name_with_retry
2121

2222
MODULE_NAME = TestId.make_unique("Persona")
2323

@@ -107,13 +107,8 @@ def test_find_persona_by_name(
107107
connection: Connection,
108108
glossary: AtlasGlossary,
109109
):
110-
result = client.asset.find_personas_by_name(MODULE_NAME)
111-
count = 0
112-
# TODO: replace with exponential back-off and jitter
113-
while not result and count < 10:
114-
time.sleep(2)
115-
result = client.asset.find_personas_by_name(MODULE_NAME)
116-
count += 1
110+
# Use centralized retry utility to handle search index consistency
111+
result = find_personas_by_name_with_retry(client, MODULE_NAME)
117112
assert result
118113
assert len(result) == 1
119114
assert result[0].guid == persona.guid

tests/integration/purpose_test.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# Copyright 2022 Atlan Pte. Ltd.
3-
import contextlib
43
import time
54
from typing import Generator
65

76
import pytest
87

98
from pyatlan.client.atlan import AtlanClient, client_connection
10-
from pyatlan.errors import NotFoundError
119
from pyatlan.model.api_tokens import ApiToken
1210
from pyatlan.model.assets import AuthPolicy, Column, Purpose
1311
from pyatlan.model.constants import SERVICE_ACCOUNT_
@@ -25,6 +23,7 @@
2523
from pyatlan.model.query import QueryRequest
2624
from tests.integration.client import TestId, delete_asset
2725
from tests.integration.requests_test import delete_token
26+
from tests.integration.utils import find_purposes_by_name_with_retry
2827

2928
MODULE_NAME = TestId.make_unique("Purpose")
3029
PERSONA_NAME = "Data Assets"
@@ -173,17 +172,10 @@ def test_find_purpose_by_name(
173172
client: AtlanClient,
174173
purpose: Purpose,
175174
):
176-
result = None
177-
with contextlib.suppress(NotFoundError):
178-
result = client.asset.find_purposes_by_name(
179-
MODULE_NAME, attributes=["purposeClassifications"]
180-
)
181-
count = 0
182-
# TODO: replace with exponential back-off and jitter
183-
while not result and count < 10:
184-
time.sleep(5)
185-
result = client.asset.find_purposes_by_name(MODULE_NAME)
186-
count += 1
175+
# Use centralized retry utility to handle search index consistency
176+
result = find_purposes_by_name_with_retry(
177+
client, MODULE_NAME, attributes=["purposeClassifications"]
178+
)
187179
assert result
188180
assert len(result) == 1
189181
assert result[0].guid == purpose.guid

tests/integration/utils.py

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1+
from typing import List, Optional
2+
13
from tenacity import (
24
retry,
35
retry_if_exception_type,
6+
retry_if_result,
47
stop_after_attempt,
8+
wait_exponential,
59
wait_random_exponential,
610
)
711

812
from pyatlan.client.atlan import AtlanClient
9-
from pyatlan.errors import AtlanError
13+
from pyatlan.errors import AtlanError, NotFoundError
14+
from pyatlan.model.assets import Persona, Purpose
15+
from pyatlan.model.fluent_search import FluentSearch
16+
from pyatlan.model.search import IndexSearchRequest
1017
from pyatlan.model.typedef import AtlanTagDef, CustomMetadataDef, EnumDef
1118

1219

@@ -35,3 +42,141 @@ def wait_for_successful_custometadatadef_purge(name: str, client: AtlanClient):
3542
)
3643
def wait_for_successful_enumadef_purge(name: str, client: AtlanClient):
3744
client.typedef.purge(name, typedef_type=EnumDef)
45+
46+
47+
# =============================
48+
# Search Retry Utilities for Eventual Consistency
49+
# =============================
50+
51+
52+
def find_personas_by_name_with_retry(
53+
client: AtlanClient, name: str, attributes: Optional[List[str]] = None
54+
) -> List[Persona]:
55+
"""
56+
Find personas by name with automatic retry for search index eventual consistency.
57+
58+
:param client: AtlanClient instance
59+
:param name: name of the persona to find
60+
:param attributes: optional attributes to retrieve
61+
:returns: list of personas found
62+
:raises NotFoundError: if no personas found after all retries
63+
"""
64+
65+
@retry(
66+
reraise=True,
67+
retry=retry_if_exception_type(NotFoundError),
68+
stop=stop_after_attempt(10),
69+
wait=wait_exponential(multiplier=1, min=2, max=10),
70+
)
71+
def _retry_find_personas():
72+
return client.asset.find_personas_by_name(name=name, attributes=attributes)
73+
74+
return _retry_find_personas()
75+
76+
77+
def find_purposes_by_name_with_retry(
78+
client: AtlanClient, name: str, attributes: Optional[List[str]] = None
79+
) -> List[Purpose]:
80+
"""
81+
Find purposes by name with automatic retry for search index eventual consistency.
82+
83+
:param client: AtlanClient instance
84+
:param name: name of the purpose to find
85+
:param attributes: optional attributes to retrieve
86+
:returns: list of purposes found
87+
:raises NotFoundError: if no purposes found after all retries
88+
"""
89+
90+
@retry(
91+
reraise=True,
92+
retry=retry_if_exception_type(NotFoundError),
93+
stop=stop_after_attempt(10),
94+
wait=wait_exponential(multiplier=1, min=2, max=10),
95+
)
96+
def _retry_find_purposes():
97+
return client.asset.find_purposes_by_name(name=name, attributes=attributes)
98+
99+
return _retry_find_purposes()
100+
101+
102+
def fluent_search_count_with_retry(
103+
fluent_search: FluentSearch, client: AtlanClient, expected_count: int
104+
) -> int:
105+
"""
106+
Count FluentSearch results with automatic retry for search index eventual consistency.
107+
108+
:param fluent_search: FluentSearch instance to count
109+
:param client: AtlanClient instance
110+
:param expected_count: expected minimum count to wait for
111+
:returns: actual count after retry logic
112+
"""
113+
114+
@retry(
115+
reraise=True,
116+
retry=retry_if_result(lambda count: count < expected_count),
117+
stop=stop_after_attempt(10),
118+
wait=wait_exponential(multiplier=1, min=2, max=10),
119+
)
120+
def _retry_count():
121+
return fluent_search.count(client)
122+
123+
return _retry_count()
124+
125+
126+
def search_request_count_with_retry(
127+
client: AtlanClient, request: IndexSearchRequest, expected_count: int
128+
) -> int:
129+
"""
130+
Count search request results with automatic retry for search index eventual consistency.
131+
132+
:param client: AtlanClient instance
133+
:param request: IndexSearchRequest to execute
134+
:param expected_count: expected minimum count to wait for
135+
:returns: actual count after retry logic
136+
"""
137+
138+
@retry(
139+
reraise=True,
140+
retry=retry_if_result(lambda count: count < expected_count),
141+
stop=stop_after_attempt(10),
142+
wait=wait_exponential(multiplier=1, min=2, max=10),
143+
)
144+
def _retry_search():
145+
response = client.asset.search(request)
146+
return response.count
147+
148+
return _retry_search()
149+
150+
151+
def assert_search_count_with_retry(
152+
client: AtlanClient, request: IndexSearchRequest, expected_count: int
153+
) -> None:
154+
"""
155+
Assert search count with retry - convenience method for test assertions.
156+
157+
:param client: AtlanClient instance
158+
:param request: IndexSearchRequest to execute
159+
:param expected_count: expected count to assert
160+
:raises AssertionError: if count doesn't match after retries
161+
"""
162+
actual_count = search_request_count_with_retry(client, request, expected_count)
163+
assert actual_count == expected_count, (
164+
f"Expected {expected_count} results, got {actual_count}"
165+
)
166+
167+
168+
def assert_fluent_search_count_with_retry(
169+
fluent_search: FluentSearch, client: AtlanClient, expected_count: int
170+
) -> None:
171+
"""
172+
Assert FluentSearch count with retry - convenience method for test assertions.
173+
174+
:param fluent_search: FluentSearch instance to count
175+
:param client: AtlanClient instance
176+
:param expected_count: expected count to assert
177+
:raises AssertionError: if count doesn't match after retries
178+
"""
179+
actual_count = fluent_search_count_with_retry(fluent_search, client, expected_count)
180+
assert actual_count == expected_count, (
181+
f"Expected {expected_count} results, got {actual_count}"
182+
)

0 commit comments

Comments
 (0)