You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by yz...@apache.org on 2026/07/21 05:51:15 UTC
(polaris) branch main updated: Fix Python CLI table command exit codes (#5091)
This is an automated email from the ASF dual-hosted git repository.
MonkeyCanCode pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new 8bf207aa30 Fix Python CLI table command exit codes (#5091)
8bf207aa30 is described below
commit 8bf207aa300b39baa1f0e68502f857c2f5af1b90
Author: Matt Faltyn <76...@users.noreply.github.com>
AuthorDate: Tue Jul 21 07:51:10 2026 +0200
Fix Python CLI table command exit codes (#5091)
Fixes #5090
---
CHANGELOG.md | 1 +
client/python/apache_polaris/cli/command/tables.py | 46 ++++++++--------------
client/python/apache_polaris/cli/polaris_cli.py | 5 ++-
client/python/tests/test_tables_command.py | 37 +++++++++++++++++
4 files changed, 58 insertions(+), 31 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ddadba6df..0fcee873f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -74,6 +74,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
- Deprecated `ALLOW_EXTERNAL_TABLE_LOCATION`. Use `ALLOW_EXTERNAL_METADATA_FILE_LOCATION` for external metadata file locations, including catalog config `polaris.config.allow.external.metadata.file.location`.
### Fixes
+- Python CLI `tables list`, `tables get`, and `tables delete` commands now exit with status 1 when catalog API requests fail. Previously, these commands printed an error but exited successfully.
- Default table storage locations with object-storage prefixing enabled are now percent-encoded with UTF-8 instead of the JVM default charset. Previously the persisted location of a table under a non-ASCII namespace or table name depended on the platform default charset, so the same table could resolve to a different (or lossy) location on a non-UTF-8 JVM.
- Async task execution (table cleanup, manifest and batch file cleanup) now retries when a handler returns false on transient errors (e.g. IO or delete failures). Previously `false` was swallowed with only a warning log and the task was never retried via the existing retry mechanism.
- `TableCleanupTaskHandler` now clamps `TABLE_METADATA_CLEANUP_BATCH_SIZE` to at least 1. Previously a non-positive realm override caused an infinite loop (0) or `IllegalArgumentException` (<0) when splitting metadata files for cleanup.
diff --git a/client/python/apache_polaris/cli/command/tables.py b/client/python/apache_polaris/cli/command/tables.py
index 33ed660086..a25b0b9569 100644
--- a/client/python/apache_polaris/cli/command/tables.py
+++ b/client/python/apache_polaris/cli/command/tables.py
@@ -77,39 +77,27 @@ class TableCommand(Command):
ns_str = UNIT_SEPARATOR.join(namespace_list)
if self.table_subcommand == Subcommands.LIST:
- try:
- result = catalog_api.list_tables(prefix=catalog_name, namespace=ns_str)
- for table_identifier in result.identifiers:
- print(table_identifier.to_json())
- except Exception as e:
- handle_api_exception(
- f"Table Listing ({'.'.join(namespace_list)})",
- e,
- )
+ result = catalog_api.list_tables(prefix=catalog_name, namespace=ns_str)
+ for table_identifier in result.identifiers:
+ print(table_identifier.to_json())
elif self.table_subcommand == Subcommands.GET:
- try:
- print(
- catalog_api.load_table(
- prefix=catalog_name,
- namespace=ns_str,
- table=table_name,
- ).to_json()
- )
- except Exception as e:
- handle_api_exception(f"Table Load ({table_name})", e)
- elif self.table_subcommand == Subcommands.DELETE:
- namespace_dot = ".".join(namespace_list)
- print(f"De-registering table {namespace_dot}.{table_name}...")
- try:
- catalog_api.drop_table(
+ print(
+ catalog_api.load_table(
prefix=catalog_name,
namespace=ns_str,
table=table_name,
- purge_requested=False,
- )
- print(f"De-registering table {namespace_dot}.{table_name} completed")
- except Exception as e:
- handle_api_exception(f"Table De-registration ({table_name})", e)
+ ).to_json()
+ )
+ elif self.table_subcommand == Subcommands.DELETE:
+ namespace_dot = ".".join(namespace_list)
+ print(f"De-registering table {namespace_dot}.{table_name}...")
+ catalog_api.drop_table(
+ prefix=catalog_name,
+ namespace=ns_str,
+ table=table_name,
+ purge_requested=False,
+ )
+ print(f"De-registering table {namespace_dot}.{table_name} completed")
elif self.table_subcommand == Subcommands.SUMMARIZE:
self._generate_summary(api, catalog_api, ns_str)
diff --git a/client/python/apache_polaris/cli/polaris_cli.py b/client/python/apache_polaris/cli/polaris_cli.py
index 546ff0f624..00c02f8381 100644
--- a/client/python/apache_polaris/cli/polaris_cli.py
+++ b/client/python/apache_polaris/cli/polaris_cli.py
@@ -30,6 +30,7 @@ from apache_polaris.cli.command import Command
from apache_polaris.cli.constants import Commands
from apache_polaris.cli.exceptions import CliError, CLI_ERROR_EXIT_CODE
from apache_polaris.cli.options.parser import Parser
+from apache_polaris.sdk.catalog.exceptions import ApiException as CatalogApiException
from apache_polaris.sdk.management import PolarisDefaultApi
from apache_polaris.sdk.management.exceptions import ApiException
from apache_polaris.cli.command.profiles import ProfilesCommand
@@ -83,7 +84,7 @@ class PolarisCli:
# Handlers from most specific to least: ApiException (OpenAPI client), CliError
# (expected user/config failures with their own exit codes), NotImplementedError
# (abstract Command misuse), then generic bugs.
- except ApiException as e:
+ except (ApiException, CatalogApiException) as e:
PolarisCli.print_api_exception(e)
sys.exit(CLI_ERROR_EXIT_CODE)
except CliError as e:
@@ -131,7 +132,7 @@ class PolarisCli:
urllib3.PoolManager.urlopen = urlopen_wrapper
@staticmethod
- def print_api_exception(e: ApiException) -> None:
+ def print_api_exception(e: ApiException | CatalogApiException) -> None:
try:
error = json.loads(e.body)["error"]
sys.stderr.write(
diff --git a/client/python/tests/test_tables_command.py b/client/python/tests/test_tables_command.py
index 9f734f907c..2f5110efd1 100644
--- a/client/python/tests/test_tables_command.py
+++ b/client/python/tests/test_tables_command.py
@@ -20,6 +20,9 @@
from unittest.mock import patch, MagicMock
from cli_test_utils import CLITestBase
from apache_polaris.cli.constants import UNIT_SEPARATOR
+from apache_polaris.cli.exceptions import CLI_ERROR_EXIT_CODE
+from apache_polaris.cli.polaris_cli import PolarisCli
+from apache_polaris.sdk.catalog.exceptions import ApiException
class TestTablesCommand(CLITestBase):
@@ -96,6 +99,40 @@ class TestTablesCommand(CLITestBase):
purge_requested=False,
)
+ @patch("apache_polaris.cli.polaris_cli.PolarisCli.print_api_exception")
+ @patch("apache_polaris.cli.command.tables.IcebergCatalogAPI")
+ @patch("apache_polaris.cli.polaris_cli.PolarisDefaultApi")
+ @patch("apache_polaris.cli.polaris_cli.ApiClientBuilder.get_api_client")
+ def test_table_api_errors_exit_with_runtime_failure(
+ self,
+ _mock_get_api_client: MagicMock,
+ mock_default_api: MagicMock,
+ mock_iceberg_api_class: MagicMock,
+ mock_print_api_exception: MagicMock,
+ ) -> None:
+ mock_default_api.return_value = self.build_mock_client()
+ mock_iceberg_api = mock_iceberg_api_class.return_value
+ cases = [
+ ("list_tables", ["tables", "list"]),
+ ("load_table", ["tables", "get", "my_table"]),
+ ("drop_table", ["tables", "delete", "my_table"]),
+ ]
+
+ for api_method, args in cases:
+ with self.subTest(api_method=api_method):
+ method = getattr(mock_iceberg_api, api_method)
+ method.side_effect = ApiException(status=404, reason="Not Found")
+
+ with self.assertRaises(SystemExit) as cm:
+ PolarisCli.execute(
+ [*args, "--catalog", "my-catalog", "--namespace", "ns1"]
+ )
+
+ self.assertEqual(cm.exception.code, CLI_ERROR_EXIT_CODE)
+ mock_print_api_exception.assert_called_once()
+ method.side_effect = None
+ mock_print_api_exception.reset_mock()
+
@patch("apache_polaris.cli.command.tables.PolicyAPI")
@patch("apache_polaris.cli.command.tables.IcebergCatalogAPI")
def test_table_summarize(