Skip to content

Commit 07b4aa9

Browse files
jacalatadependabot[bot]jimjag
authored
refreshextracts refreshes the wrong extract if you have multiple data sources with the same name and same Project name (#376)
* Bump actions/checkout from 4 to 5 (#362) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions/setup-python from 5 to 6 (#363) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Upload required file(s) for compliance * Update CODEOWNERS to remove '404: Not Found' * add parent project to the search for items * add test This test more explicitly documents the expected behavior * address feedback from copilot --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jim Jagielski <jimjag@gmail.com> Co-authored-by: Jim Jagielski <jjagielski@salesforce.com>
1 parent 903edae commit 07b4aa9

3 files changed

Lines changed: 118 additions & 4 deletions

File tree

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#ECCN:Open Source

tabcmd/commands/server.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional
5252
logger.debug(_("export.status").format(item_log_name))
5353

5454
result = []
55-
total_available_items = None
5655
page_number = 1
5756
total_retrieved_items = 0
5857

@@ -62,9 +61,9 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional
6261
TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, item_name)
6362
)
6463

65-
# todo - this doesn't filter if the project is in the top level.
66-
# todo: there is no guarantee that these fields are the same for different content types.
67-
# probably better if we move that type specific logic out to a wrapper
64+
# When a container (project) is provided, add a parent filter to narrow results coming back
65+
# from the server, then additionally post-filter client-side by project_id to disambiguate
66+
# cases where multiple projects share the same name under different parent paths.
6867
if container:
6968
# the name of the filter field is different if you are finding a project or any other item
7069
if type(item_endpoint).__name__.find("Projects") < 0:
@@ -87,6 +86,10 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional
8786
)
8887

8988
total_retrieved_items += len(all_items)
89+
# Post-filter by exact project_id when looking up non-project items within a specific container.
90+
# This prevents selecting a similarly named item from a sibling project that shares the same name.
91+
if container and type(item_endpoint).__name__.find("Projects") < 0:
92+
all_items = [i for i in all_items if getattr(i, "project_id", None) == container.id]
9093

9194
logger.debug(
9295
"{} items of name: {} were found for query page number: {}, page size: {} & total available: {}".format(
@@ -99,11 +102,27 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional
99102
)
100103

101104
result.extend(all_items)
105+
106+
# Once we have disambiguated items for a specific container/project, there is no need to
107+
# continue paginating further since additional pages would return the same name-matched
108+
# items from other projects which are filtered out, leading to duplicates.
109+
if container and type(item_endpoint).__name__.find("Projects") < 0 and len(all_items) > 0:
110+
break
111+
102112
if total_retrieved_items >= pagination_item.total_available:
103113
break
104114

105115
page_number = pagination_item.page_number + 1
106116

117+
# If a container was provided and no items remained after disambiguation by project_id,
118+
# surface a not-found to align with server-side not-found semantics.
119+
if container and type(item_endpoint).__name__.find("Projects") < 0 and len(result) == 0:
120+
raise TSC.ServerResponseError(
121+
code="404",
122+
summary=_("errors.xmlapi.not_found"),
123+
detail=_("errors.xmlapi.not_found") + ": " + item_log_name,
124+
)
125+
107126
return result
108127

109128
# Get site by name or get currently logged in site
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import pytest
2+
import tableauserverclient as TSC
3+
4+
from tabcmd.commands.server import Server
5+
6+
7+
class _Logger:
8+
def debug(self, *_args, **_kwargs):
9+
pass
10+
11+
12+
class _DummyItem:
13+
def __init__(self, name: str, project_id: str):
14+
self.name = name
15+
self.project_id = project_id
16+
17+
18+
class _DummyPagination:
19+
def __init__(self, total_available: int, page_number: int = 1, page_size: int = 100):
20+
self.total_available = total_available
21+
self.page_number = page_number
22+
self.page_size = page_size
23+
24+
25+
class _DatasourcesEndpoint:
26+
def __init__(self, items):
27+
self._items = items
28+
29+
def get(self, _req_option: TSC.RequestOptions):
30+
# Ignore server-side filters for this unit test; we validate client-side disambiguation
31+
return self._items, _DummyPagination(total_available=len(self._items))
32+
33+
34+
class _ProjectItem:
35+
# Minimal project-like object carrying id/name used by Server.get_items_by_name
36+
def __init__(self, project_id: str, name: str):
37+
self.id = project_id
38+
self.name = name
39+
40+
41+
def test_filters_datasources_by_exact_project_id_when_container_provided():
42+
logger = _Logger()
43+
container = _ProjectItem(project_id="proj-A", name="Shared")
44+
# Two datasources with identical names, different project ownership
45+
items = [
46+
_DummyItem(name="Sales", project_id="proj-A"),
47+
_DummyItem(name="Sales", project_id="proj-B"),
48+
]
49+
endpoint = _DatasourcesEndpoint(items)
50+
51+
results = Server.get_items_by_name(logger, endpoint, "Sales", container)
52+
53+
assert len(results) == 1
54+
assert results[0].project_id == "proj-A"
55+
56+
57+
def test_raises_not_found_when_no_items_match_container_after_disambiguation():
58+
logger = _Logger()
59+
container = _ProjectItem(project_id="proj-Z", name="Shared")
60+
items = [
61+
_DummyItem(name="Sales", project_id="proj-A"),
62+
_DummyItem(name="Sales", project_id="proj-B"),
63+
]
64+
endpoint = _DatasourcesEndpoint(items)
65+
66+
with pytest.raises(TSC.ServerResponseError):
67+
Server.get_items_by_name(logger, endpoint, "Sales", container)
68+
69+
70+
def test_nested_projects_same_leaf_name_returns_correct_datasource_per_container():
71+
logger = _Logger()
72+
# Simulate three separate 'Cats' projects that exist at different levels:
73+
# MyProjects/ProjectA/Cats, MyProjects/ProjectB/Cats, and MyProjects/Cats
74+
cats_under_project_a = _ProjectItem(project_id="cats-A", name="Cats")
75+
cats_under_project_b = _ProjectItem(project_id="cats-B", name="Cats")
76+
cats_under_root = _ProjectItem(project_id="cats-root", name="Cats")
77+
78+
# Three datasources all named identically but owned by different 'Cats' projects
79+
items = [
80+
_DummyItem(name="my-datasource", project_id="cats-A"),
81+
_DummyItem(name="my-datasource", project_id="cats-B"),
82+
_DummyItem(name="my-datasource", project_id="cats-root"),
83+
]
84+
endpoint = _DatasourcesEndpoint(items)
85+
86+
# Each lookup should return exactly one item from the target project id
87+
res_a = Server.get_items_by_name(logger, endpoint, "my-datasource", cats_under_project_a)
88+
assert len(res_a) == 1 and res_a[0].project_id == "cats-A"
89+
90+
res_b = Server.get_items_by_name(logger, endpoint, "my-datasource", cats_under_project_b)
91+
assert len(res_b) == 1 and res_b[0].project_id == "cats-B"
92+
93+
res_root = Server.get_items_by_name(logger, endpoint, "my-datasource", cats_under_root)
94+
assert len(res_root) == 1 and res_root[0].project_id == "cats-root"

0 commit comments

Comments
 (0)