From f3829df92a317a3588fa387f1bdd7333ea15f99f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:22:08 -0700 Subject: [PATCH 1/7] 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](https://github.com/actions/checkout/compare/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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check-coverage.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/generate-metadata.yml | 2 +- .github/workflows/package.yml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/run-e2-tests.yml | 2 +- .github/workflows/run-tests.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 3d4996e4..46bf83ab 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -18,7 +18,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} uses: actions/setup-python@v5 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0cb6f814..06d92ba4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/generate-metadata.yml b/.github/workflows/generate-metadata.yml index 53449eb9..ac809320 100644 --- a/.github/workflows/generate-metadata.yml +++ b/.github/workflows/generate-metadata.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index d9b65a93..ed9d2752 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -60,7 +60,7 @@ jobs: UPLOAD_FILE_NAME: tabcmd steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index bc101762..6fa73f02 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -15,7 +15,7 @@ jobs: name: Build dist files for PyPi runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: actions/setup-python@v5 diff --git a/.github/workflows/run-e2-tests.yml b/.github/workflows/run-e2-tests.yml index 81ebb4f6..4ae77785 100644 --- a/.github/workflows/run-e2-tests.yml +++ b/.github/workflows/run-e2-tests.yml @@ -23,7 +23,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} uses: actions/setup-python@v5 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 4e9d279a..df7cdc7f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -20,7 +20,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} uses: actions/setup-python@v5 From e9c640c45cb5cf1152ec0ff79597c4cf00da5553 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:52:52 -0700 Subject: [PATCH 2/7] 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](https://github.com/actions/setup-python/compare/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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check-coverage.yml | 2 +- .github/workflows/generate-metadata.yml | 2 +- .github/workflows/package.yml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/python-app.yml | 2 +- .github/workflows/run-e2-tests.yml | 2 +- .github/workflows/run-tests.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 46bf83ab..fe9b244d 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/generate-metadata.yml b/.github/workflows/generate-metadata.yml index ac809320..7e59af3e 100644 --- a/.github/workflows/generate-metadata.yml +++ b/.github/workflows/generate-metadata.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index ed9d2752..fe851902 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -62,7 +62,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.12 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 6fa73f02..96400505 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.12 - name: Build dist files diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 023a4a7d..c294aff4 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: pip install Tabcmd diff --git a/.github/workflows/run-e2-tests.yml b/.github/workflows/run-e2-tests.yml index 4ae77785..0f04cdab 100644 --- a/.github/workflows/run-e2-tests.yml +++ b/.github/workflows/run-e2-tests.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index df7cdc7f..fca98373 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} From 3cd02655fd27be6c5d0063a5468d95325866c377 Mon Sep 17 00:00:00 2001 From: Jim Jagielski Date: Mon, 10 Nov 2025 09:50:23 -0500 Subject: [PATCH 3/7] Upload required file(s) for compliance --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..8d46b0b4 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +404: Not Found#ECCN:Open Source From c10b67e4ea1b0ff82384e49296324934ba30d43f Mon Sep 17 00:00:00 2001 From: Jim Jagielski Date: Mon, 24 Nov 2025 08:33:40 -0500 Subject: [PATCH 4/7] Update CODEOWNERS to remove '404: Not Found' --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8d46b0b4..00cd7bd1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -404: Not Found#ECCN:Open Source +#ECCN:Open Source From db8eaef575f672dff6e40fda21233772f30039c2 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 12 Feb 2026 18:18:45 -0800 Subject: [PATCH 5/7] add parent project to the search for items --- tabcmd/commands/server.py | 27 +++++++- tests/commands/test_server_item_selection.py | 68 ++++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tests/commands/test_server_item_selection.py diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index 19136141..942d3bd7 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -62,9 +62,9 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, item_name) ) - # todo - this doesn't filter if the project is in the top level. - # todo: there is no guarantee that these fields are the same for different content types. - # probably better if we move that type specific logic out to a wrapper + # When a container (project) is provided, add a parent filter to narrow results coming back + # from the server, then additionally post-filter client-side by project_id to disambiguate + # cases where multiple projects share the same name under different parent paths. if container: # the name of the filter field is different if you are finding a project or any other item if type(item_endpoint).__name__.find("Projects") < 0: @@ -86,6 +86,11 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional detail=_("errors.xmlapi.not_found") + ": " + item_log_name, ) + # Post-filter by exact project_id when looking up non-project items within a specific container. + # This prevents selecting a similarly named item from a sibling project that shares the same name. + if container and type(item_endpoint).__name__.find("Projects") < 0: + all_items = [i for i in all_items if getattr(i, "project_id", None) == container.id] + total_retrieved_items += len(all_items) logger.debug( @@ -99,11 +104,27 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional ) result.extend(all_items) + + # Once we have disambiguated items for a specific container/project, there is no need to + # continue paginating further since additional pages would return the same name-matched + # items from other projects which are filtered out, leading to duplicates. + if container and type(item_endpoint).__name__.find("Projects") < 0 and len(all_items) > 0: + break + if total_retrieved_items >= pagination_item.total_available: break page_number = pagination_item.page_number + 1 + # If a container was provided and no items remained after disambiguation by project_id, + # surface a not-found to align with server-side not-found semantics. + if container and type(item_endpoint).__name__.find("Projects") < 0 and len(result) == 0: + raise TSC.ServerResponseError( + code="404", + summary=_("errors.xmlapi.not_found"), + detail=_("errors.xmlapi.not_found") + ": " + item_log_name, + ) + return result # Get site by name or get currently logged in site diff --git a/tests/commands/test_server_item_selection.py b/tests/commands/test_server_item_selection.py new file mode 100644 index 00000000..e4c32025 --- /dev/null +++ b/tests/commands/test_server_item_selection.py @@ -0,0 +1,68 @@ +import pytest +import tableauserverclient as TSC + +from tabcmd.commands.server import Server + + +class _Logger: + def debug(self, *_args, **_kwargs): + pass + + +class _DummyItem: + def __init__(self, name: str, project_id: str): + self.name = name + self.project_id = project_id + + +class _DummyPagination: + def __init__(self, total_available: int, page_number: int = 1, page_size: int = 100): + self.total_available = total_available + self.page_number = page_number + self.page_size = page_size + + +class _DatasourcesEndpoint: + # Deliberately named so type(item_endpoint).__name__ == "Datasources" + def __init__(self, items): + self._items = items + + def get(self, _req_option: TSC.RequestOptions): + # Ignore server-side filters for this unit test; we validate client-side disambiguation + return self._items, _DummyPagination(total_available=len(self._items)) + + +class _ProjectItem: + # Minimal project-like object carrying id/name used by Server.get_items_by_name + def __init__(self, project_id: str, name: str): + self.id = project_id + self.name = name + + +def test_filters_datasources_by_exact_project_id_when_container_provided(): + logger = _Logger() + container = _ProjectItem(project_id="proj-A", name="Shared") + # Two datasources with identical names, different project ownership + items = [ + _DummyItem(name="Sales", project_id="proj-A"), + _DummyItem(name="Sales", project_id="proj-B"), + ] + endpoint = _DatasourcesEndpoint(items) + + results = Server.get_items_by_name(logger, endpoint, "Sales", container) + + assert len(results) == 1 + assert results[0].project_id == "proj-A" + + +def test_raises_not_found_when_no_items_match_container_after_disambiguation(): + logger = _Logger() + container = _ProjectItem(project_id="proj-Z", name="Shared") + items = [ + _DummyItem(name="Sales", project_id="proj-A"), + _DummyItem(name="Sales", project_id="proj-B"), + ] + endpoint = _DatasourcesEndpoint(items) + + with pytest.raises(TSC.ServerResponseError): + Server.get_items_by_name(logger, endpoint, "Sales", container) From 2d73f49a99520882154ab16d586694ae08d81f5a Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 12 Feb 2026 20:03:42 -0800 Subject: [PATCH 6/7] add test This test more explicitly documents the expected behavior --- tests/commands/test_server_item_selection.py | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/commands/test_server_item_selection.py b/tests/commands/test_server_item_selection.py index e4c32025..1e73df8a 100644 --- a/tests/commands/test_server_item_selection.py +++ b/tests/commands/test_server_item_selection.py @@ -66,3 +66,30 @@ def test_raises_not_found_when_no_items_match_container_after_disambiguation(): with pytest.raises(TSC.ServerResponseError): Server.get_items_by_name(logger, endpoint, "Sales", container) + + +def test_nested_projects_same_leaf_name_returns_correct_datasource_per_container(): + logger = _Logger() + # Simulate three separate 'Cats' projects that exist at different levels: + # MyProjects/ProjectA/Cats, MyProjects/ProjectB/Cats, and MyProjects/Cats + cats_under_project_a = _ProjectItem(project_id="cats-A", name="Cats") + cats_under_project_b = _ProjectItem(project_id="cats-B", name="Cats") + cats_under_root = _ProjectItem(project_id="cats-root", name="Cats") + + # Three datasources all named identically but owned by different 'Cats' projects + items = [ + _DummyItem(name="my-datasource", project_id="cats-A"), + _DummyItem(name="my-datasource", project_id="cats-B"), + _DummyItem(name="my-datasource", project_id="cats-root"), + ] + endpoint = _DatasourcesEndpoint(items) + + # Each lookup should return exactly one item from the target project id + res_a = Server.get_items_by_name(logger, endpoint, "my-datasource", cats_under_project_a) + assert len(res_a) == 1 and res_a[0].project_id == "cats-A" + + res_b = Server.get_items_by_name(logger, endpoint, "my-datasource", cats_under_project_b) + assert len(res_b) == 1 and res_b[0].project_id == "cats-B" + + res_root = Server.get_items_by_name(logger, endpoint, "my-datasource", cats_under_root) + assert len(res_root) == 1 and res_root[0].project_id == "cats-root" From 8088aa535d6aa304f935daabbb8ad654decb3a1e Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 12 Feb 2026 22:43:28 -0800 Subject: [PATCH 7/7] address feedback from copilot --- tabcmd/commands/server.py | 4 +--- tests/commands/test_server_item_selection.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index 942d3bd7..eb466183 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -52,7 +52,6 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional logger.debug(_("export.status").format(item_log_name)) result = [] - total_available_items = None page_number = 1 total_retrieved_items = 0 @@ -86,13 +85,12 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional detail=_("errors.xmlapi.not_found") + ": " + item_log_name, ) + total_retrieved_items += len(all_items) # Post-filter by exact project_id when looking up non-project items within a specific container. # This prevents selecting a similarly named item from a sibling project that shares the same name. if container and type(item_endpoint).__name__.find("Projects") < 0: all_items = [i for i in all_items if getattr(i, "project_id", None) == container.id] - total_retrieved_items += len(all_items) - logger.debug( "{} items of name: {} were found for query page number: {}, page size: {} & total available: {}".format( len(all_items), diff --git a/tests/commands/test_server_item_selection.py b/tests/commands/test_server_item_selection.py index 1e73df8a..c9d76df8 100644 --- a/tests/commands/test_server_item_selection.py +++ b/tests/commands/test_server_item_selection.py @@ -23,7 +23,6 @@ def __init__(self, total_available: int, page_number: int = 1, page_size: int = class _DatasourcesEndpoint: - # Deliberately named so type(item_endpoint).__name__ == "Datasources" def __init__(self, items): self._items = items