From c138a1c26704d1d10a17f985caac31019f325bb8 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 8 Sep 2026 16:54:07 -0700 Subject: [PATCH 1/9] Require --db-server when publishing with embedded credentials PR #458 fixed the two publish crashes when using --db-username / --db-password / --save-db-password, but callers who omit --db-server still hit a raw ValueError from tableauserverclient's _add_connections_element. Fail fast in run_command with a clear, localizable message before we build the ConnectionItem so users get an actionable error instead of an internal stack trace. Update the existing no-db-server test to assert the exit path and add the parallel coverage for the --oauth-username branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../publish_command.py | 4 ++ .../locales/en/tabcmd_messages_en.properties | 1 + tests/commands/test_publish_command.py | 42 +++++++++++++++---- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index dfd26b28..40d90160 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -46,6 +46,10 @@ def define_args(publish_parser): def run_command(cls, args): logger = log(cls.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) + + if (args.db_username or args.oauth_username) and not args.db_server: + Errors.exit_with_error(logger, _("publish.errors.db_server_required")) + session = Session() server = session.create_session(args, logger) diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index e5274230..2624e726 100644 --- a/tabcmd/locales/en/tabcmd_messages_en.properties +++ b/tabcmd/locales/en/tabcmd_messages_en.properties @@ -75,6 +75,7 @@ logout.short_description=Sign out from the server publish.description=Publish a workbook, data source, or extract to the server publish.errors.unexpected_server_response=Unexpected response from the server: {0} publish.errors.server_resource_not_found=The resource you specified does not exist, or you do not have permission to see it. Check your project name and permissions +publish.errors.db_server_required=--db-server is required when publishing a workbook with --db-username or --oauth-username. Use --db-server to specify the database server address the embedded credentials should be associated with. publish.options.append=Append extract file to existing data source publish.options.db-password=Database password for all data sources publish.options.db-server=Server address of the database to associate with the embedded connection credentials. Required when using --db-username or --oauth-username to publish a workbook. diff --git a/tests/commands/test_publish_command.py b/tests/commands/test_publish_command.py index df4b7115..57e2ef9a 100644 --- a/tests/commands/test_publish_command.py +++ b/tests/commands/test_publish_command.py @@ -72,7 +72,7 @@ def test_publish_with_creds(self, mock_path, mock_glob, mock_session): self.assertEqual(len(connections), 1) self.assertEqual(connections[0].server_address, "db.example.com") - def test_publish_with_creds_no_db_server(self, mock_path, mock_glob, mock_session): + def test_publish_with_db_username_missing_db_server_exits(self, mock_path, mock_glob, mock_session): set_up_mock_server(mock_session) mock_path = set_up_mock_path(mock_path) @@ -87,7 +87,7 @@ def test_publish_with_creds_no_db_server(self, mock_path, mock_glob, mock_sessio mock_args.tabbed = True mock_args.db_username = "username" - mock_args.db_password = "oauth_u" + mock_args.db_password = "password" mock_args.db_server = None mock_args.save_db_password = True mock_args.oauth_username = None @@ -97,13 +97,39 @@ def test_publish_with_creds_no_db_server(self, mock_path, mock_glob, mock_sessio mock_args.thumbnail_group = None mock_args.skip_connection_check = False - PublishCommand.run_command(mock_args) - mock_session.internal_server.workbooks.publish.assert_called() + with self.assertRaises(SystemExit): + PublishCommand.run_command(mock_args) + mock_session.internal_server.workbooks.publish.assert_not_called() - call_kwargs = mock_session.internal_server.workbooks.publish.call_args.kwargs - connections = call_kwargs["connections"] - self.assertEqual(len(connections), 1) - self.assertIsNone(connections[0].server_address) + def test_publish_with_oauth_username_missing_db_server_exits(self, mock_path, mock_glob, mock_session): + set_up_mock_server(mock_session) + mock_path = set_up_mock_path(mock_path) + + mock_args.overwrite = False + mock_args.append = True + mock_args.replace = False + + mock_args.filename = "existing_file.twbx" + mock_args.project_name = "project-name" + mock_args.parent_project_path = "projects" + mock_args.name = "" + mock_args.tabbed = True + + mock_args.db_username = None + mock_args.db_password = None + mock_args.db_server = None + mock_args.save_db_password = False + mock_args.oauth_username = "oauth_user" + mock_args.save_oauth = True + mock_args.embed = False + + mock_args.thumbnail_username = None + mock_args.thumbnail_group = None + mock_args.skip_connection_check = False + + with self.assertRaises(SystemExit): + PublishCommand.run_command(mock_args) + mock_session.internal_server.workbooks.publish.assert_not_called() def test_get_files_to_publish_twbx(self, mock_path, mock_glob, mock_session): set_up_mock_server(mock_session) From 53d7c5d15a5735571828aca4c5691ffd77876820 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 8 Sep 2026 17:04:32 -0700 Subject: [PATCH 2/9] Add e2e coverage for --db-server requirement Extend the online publish suite with: - A positive assertion for the happy path: test_wb_publish_embedded now passes --db-server matching the connection host baked into EmbeddedCredentials.twb (see-internal-slack). Without a matching server_address, tableauserverclient silently drops the embedded credentials at publish time, so covering the match is what actually proves the fix. - A negative assertion: test_wb_publish_embedded_missing_db_server_fails runs publish with --db-username but no --db-server and expects a non-zero exit. The check now fires in run_command before any network work, so hardcoded placeholder creds are sufficient. Threads a db_server keyword through the _publish_creds_args helper and records the workbook's connection host as a class constant. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/e2e/online_tests.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index 741af663..b4e7a6fb 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -75,6 +75,9 @@ def get_publishable_name(file_value: str) -> str: TDS_FILE_LIVE = "live_mysql.tds" TWB_FILE_WITH_EMBEDDED_CONNECTION = "EmbeddedCredentials.twb" + # server_address baked into the workbook's element; the server-side + # embed only sticks when --db-server matches this value. + TWB_FILE_EMBEDDED_CONNECTION_SERVER = "see-internal-slack" USERS_DETAILS_FILE = "detailed_users.csv" USERNAMES_FILE = "usernames.csv" @@ -142,7 +145,9 @@ def _publish_args(file, name, optional_args=None): return arguments @staticmethod - def _publish_creds_args(arguments, db_user=None, db_pass=None, db_save=None, oauth_user=None, oauth_save=None): + def _publish_creds_args( + arguments, db_user=None, db_pass=None, db_save=None, oauth_user=None, oauth_save=None, db_server=None + ): if db_user: arguments.append("--db-username") arguments.append(db_user) @@ -156,6 +161,9 @@ def _publish_creds_args(arguments, db_user=None, db_pass=None, db_save=None, oau arguments.append(oauth_user) if oauth_save: arguments.append("--save-oauth") + if db_server: + arguments.append("--db-server") + arguments.append(db_server) return arguments @staticmethod @@ -471,11 +479,32 @@ def test_wb_publish_embedded(self): file = os.path.join("tests", "assets", TestAssets.TWB_FILE_WITH_EMBEDDED_CONNECTION) name_on_server = TestAssets.get_publishable_name(TestAssets.TWB_FILE_WITH_EMBEDDED_CONNECTION) arguments = TabcmdCall._publish_args(file, name_on_server) - arguments = TabcmdCall._publish_creds_args(arguments, database_user, database_password, True) + arguments = TabcmdCall._publish_creds_args( + arguments, + database_user, + database_password, + True, + db_server=TestAssets.TWB_FILE_EMBEDDED_CONNECTION_SERVER, + ) arguments.append("--tabbed") arguments.append("--skip-connection-check") _test_command(arguments) + @pytest.mark.order(11) + def test_wb_publish_embedded_missing_db_server_fails(self): + # publish with --db-username but no --db-server must exit non-zero with + # a friendly message rather than a raw tableauserverclient traceback. + # Hardcoded placeholder creds are fine here: the check fires in + # run_command before we touch the database. + file = os.path.join("tests", "assets", TestAssets.TWB_FILE_WITH_EMBEDDED_CONNECTION) + name_on_server = TestAssets.get_publishable_name(TestAssets.TWB_FILE_WITH_EMBEDDED_CONNECTION) + "-no-server" + arguments = TabcmdCall._publish_args(file, name_on_server) + arguments = TabcmdCall._publish_creds_args(arguments, "placeholder_user", "placeholder_pass", True) + arguments.append("--tabbed") + arguments.append("--skip-connection-check") + with pytest.raises(subprocess.CalledProcessError): + _test_command(arguments) + @pytest.mark.order(12) def test_publish_ds(self): file = os.path.join("tests", "assets", TestAssets.TDSX_FILE_WITH_EXTRACT) From 9c64d74dc27d9f2cbaa57b2757f108313c167e92 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 8 Sep 2026 22:24:12 -0700 Subject: [PATCH 3/9] Address code-review findings on the --db-server guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scope the guard to workbook publishes only. Datasource publishes go through _add_credentials_element on the TSC side, which never requires server_address, so the previous unconditional check was a regression for `tabcmd publish live_mysql.tds --db-username ...` and similar. The guard now checks the target filename extension early (before auth, for fast failure) and repeats per-file inside the workbook branch of the loop for the folder-publish case. Rename the `credentials` parameter of `publish_workbook_file` to `connection` — it holds a single ConnectionItem, not a list. Tests: - Add test_publish_with_oauth_creds covering the oauth branch's server_address assignment (previously untested). - Add test_publish_datasource_with_db_username_no_db_server verifying datasource publishes are not blocked when --db-server is omitted. - Strengthen the e2e negative test to run tabcmd via subprocess.run, capture stdout/stderr, and assert on the localized guard message (or the raw key when .mo has not been regenerated). Any earlier unrelated failure — bad auth, missing asset, session expiry — now fails the test rather than passing it. - Trim the misleading class-constant comment on TWB_FILE_EMBEDDED_ CONNECTION_SERVER; reword the "before we touch the database" comment to reference the guard's contract instead of a specific location. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../publish_command.py | 18 +++-- tests/commands/test_publish_command.py | 72 +++++++++++++++++++ tests/e2e/online_tests.py | 25 ++++--- 3 files changed, 103 insertions(+), 12 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index 40d90160..46945659 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -47,8 +47,14 @@ def run_command(cls, args): logger = log(cls.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) + # Fail fast before auth when the direct target is clearly a workbook file: + # TSC's workbook publish path requires ConnectionItem.server_address whenever + # embedded credentials are supplied. Datasource publishes never need it, so + # this check is deliberately scoped to workbook extensions. if (args.db_username or args.oauth_username) and not args.db_server: - Errors.exit_with_error(logger, _("publish.errors.db_server_required")) + filename = (args.filename or "").lower() + if filename.endswith(".twb") or filename.endswith(".twbx"): + Errors.exit_with_error(logger, _("publish.errors.db_server_required")) session = Session() server = session.create_session(args, logger) @@ -92,6 +98,10 @@ def run_command(cls, args): source = PublishCommand.get_filename_extension_if_tableau_type(logger, str_filename) logger.info(_("publish.status").format(str_filename)) if source in ["twbx", "twb"]: + # TSC's workbook publish path requires ConnectionItem.server_address whenever + # embedded connection credentials are supplied; datasource publish does not. + if (args.db_username or args.oauth_username) and not args.db_server: + Errors.exit_with_error(logger, _("publish.errors.db_server_required")) try: published_item = PublishCommand.publish_workbook_file( args=args, @@ -100,7 +110,7 @@ def run_command(cls, args): project_id=project_id, str_filename=str_filename, publish_mode=publish_mode, - credentials=workbook_connections, + connection=workbook_connections, ) except Exception as e: Errors.exit_with_error(logger, exception=e) @@ -176,7 +186,7 @@ def get_publish_mode(args, logger): return publish_mode @staticmethod - def publish_workbook_file(args, logger, server, project_id, str_filename, publish_mode, credentials): + def publish_workbook_file(args, logger, server, project_id, str_filename, publish_mode, connection): if args.thumbnail_group: raise AttributeError("Generating thumbnails for a group is not yet implemented.") if args.thumbnail_username and args.thumbnail_group: @@ -189,7 +199,7 @@ def publish_workbook_file(args, logger, server, project_id, str_filename, publis publish_mode, # args.thumbnail_username, not yet implemented in tsc # args.thumbnail_group, - connections=[credentials] if credentials else None, + connections=[connection] if connection else None, as_job=False, skip_connection_check=args.skip_connection_check, ) diff --git a/tests/commands/test_publish_command.py b/tests/commands/test_publish_command.py index 57e2ef9a..5c54bc6e 100644 --- a/tests/commands/test_publish_command.py +++ b/tests/commands/test_publish_command.py @@ -131,6 +131,78 @@ def test_publish_with_oauth_username_missing_db_server_exits(self, mock_path, mo PublishCommand.run_command(mock_args) mock_session.internal_server.workbooks.publish.assert_not_called() + def test_publish_with_oauth_creds(self, mock_path, mock_glob, mock_session): + set_up_mock_server(mock_session) + mock_path = set_up_mock_path(mock_path) + + mock_args.overwrite = False + mock_args.append = True + mock_args.replace = False + + mock_args.filename = "existing_file.twbx" + mock_args.project_name = "project-name" + mock_args.parent_project_path = "projects" + mock_args.name = "" + mock_args.tabbed = True + + mock_args.db_username = None + mock_args.db_password = None + mock_args.save_db_password = False + mock_args.oauth_username = "oauth_user" + mock_args.save_oauth = True + mock_args.db_server = "db.example.com" + mock_args.embed = False + + mock_args.thumbnail_username = None + mock_args.thumbnail_group = None + mock_args.skip_connection_check = False + + PublishCommand.run_command(mock_args) + mock_session.internal_server.workbooks.publish.assert_called() + + call_kwargs = mock_session.internal_server.workbooks.publish.call_args.kwargs + connections = call_kwargs["connections"] + self.assertEqual(len(connections), 1) + self.assertEqual(connections[0].server_address, "db.example.com") + + def test_publish_datasource_with_db_username_no_db_server(self, mock_path, mock_glob, mock_session): + # Datasource publishes send credentials via _add_credentials_element on the TSC side, + # which does not require server_address. --db-server must not be required here. + set_up_mock_server(mock_session) + # set_up_mock_server only wires .workbooks on the mocked server (TSC.Server class-spec + # doesn't expose instance attributes from __init__); wire .datasources by hand. + mock_session.internal_server.datasources = mock.MagicMock() + mock_path = set_up_mock_path(mock_path) + # set_up_mock_path hardcodes splitext -> [_, 'twbx']; override so extension routing + # actually sees a datasource here. + mock_path.splitext = lambda x: ["file", "tdsx"] + + mock_args.overwrite = False + mock_args.append = True + mock_args.replace = False + + mock_args.filename = "existing_file.tdsx" + mock_args.project_name = "project-name" + mock_args.parent_project_path = "projects" + mock_args.name = "" + mock_args.tabbed = True + + mock_args.db_username = "username" + mock_args.db_password = "password" + mock_args.db_server = None + mock_args.save_db_password = True + mock_args.oauth_username = None + mock_args.embed = False + mock_args.use_tableau_bridge = False + + mock_args.thumbnail_username = None + mock_args.thumbnail_group = None + mock_args.skip_connection_check = False + + PublishCommand.run_command(mock_args) + mock_session.internal_server.datasources.publish.assert_called() + mock_session.internal_server.workbooks.publish.assert_not_called() + def test_get_files_to_publish_twbx(self, mock_path, mock_glob, mock_session): set_up_mock_server(mock_session) mock_path = set_up_mock_path(mock_path) diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index b4e7a6fb..b8ea4bfa 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -75,8 +75,7 @@ def get_publishable_name(file_value: str) -> str: TDS_FILE_LIVE = "live_mysql.tds" TWB_FILE_WITH_EMBEDDED_CONNECTION = "EmbeddedCredentials.twb" - # server_address baked into the workbook's element; the server-side - # embed only sticks when --db-server matches this value. + # server_address baked into the workbook's element. TWB_FILE_EMBEDDED_CONNECTION_SERVER = "see-internal-slack" USERS_DETAILS_FILE = "detailed_users.csv" @@ -492,18 +491,28 @@ def test_wb_publish_embedded(self): @pytest.mark.order(11) def test_wb_publish_embedded_missing_db_server_fails(self): - # publish with --db-username but no --db-server must exit non-zero with - # a friendly message rather than a raw tableauserverclient traceback. - # Hardcoded placeholder creds are fine here: the check fires in - # run_command before we touch the database. + # Publish with --db-username but no --db-server must exit non-zero with our + # friendly message rather than a raw tableauserverclient traceback. The guard + # short-circuits before any real work, so throwaway credentials are safe here. file = os.path.join("tests", "assets", TestAssets.TWB_FILE_WITH_EMBEDDED_CONNECTION) name_on_server = TestAssets.get_publishable_name(TestAssets.TWB_FILE_WITH_EMBEDDED_CONNECTION) + "-no-server" arguments = TabcmdCall._publish_args(file, name_on_server) arguments = TabcmdCall._publish_creds_args(arguments, "placeholder_user", "placeholder_pass", True) arguments.append("--tabbed") arguments.append("--skip-connection-check") - with pytest.raises(subprocess.CalledProcessError): - _test_command(arguments) + + login_args = setup_e2e.get_login_args() + if login_args is None: + pytest.skip("No credentials available (credentials.py not found)") + calling_args = ["python", "-m", "tabcmd"] + arguments + login_args + [debug_log] + ["--no-certcheck"] + result = subprocess.run(calling_args, capture_output=True, text=True) + + assert result.returncode != 0, "expected non-zero exit for missing --db-server" + # Localized string OR the raw key (if .mo has not been regenerated yet) both signal our guard. + combined = (result.stdout or "") + (result.stderr or "") + assert "publish.errors.db_server_required" in combined or "--db-server is required" in combined, ( + "expected guard message in output; got:\n" + combined + ) @pytest.mark.order(12) def test_publish_ds(self): From 806c53ae49c9b64acc5aab2797069d4946d65680 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 10 Sep 2026 14:43:11 -0700 Subject: [PATCH 4/9] Extract _require_db_server_for_workbook helper (M2) The guard against workbook publish with embedded credentials but no --db-server was duplicated in run_command (early pre-auth check and in-loop per-file check). Extract it into a single classmethod so the two call sites cannot drift. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../publish_command.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index 46945659..0a94cacb 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -42,6 +42,16 @@ def define_args(publish_parser): set_append_replace_option(group) set_parent_project_arg(group) + @classmethod + def _require_db_server_for_workbook(cls, args, filename, logger): + """Fail fast if the caller is publishing a workbook with embedded + credentials but did not supply --db-server. Called both up front + (direct-file case) and inside the per-file publish loop (folder case).""" + if (args.db_username or args.oauth_username) and not args.db_server: + filename_lower = (filename or "").lower() + if filename_lower.endswith(".twb") or filename_lower.endswith(".twbx"): + Errors.exit_with_error(logger, _("publish.errors.db_server_required")) + @classmethod def run_command(cls, args): logger = log(cls.__name__, args.logging_level) @@ -51,10 +61,7 @@ def run_command(cls, args): # TSC's workbook publish path requires ConnectionItem.server_address whenever # embedded credentials are supplied. Datasource publishes never need it, so # this check is deliberately scoped to workbook extensions. - if (args.db_username or args.oauth_username) and not args.db_server: - filename = (args.filename or "").lower() - if filename.endswith(".twb") or filename.endswith(".twbx"): - Errors.exit_with_error(logger, _("publish.errors.db_server_required")) + PublishCommand._require_db_server_for_workbook(args, args.filename, logger) session = Session() server = session.create_session(args, logger) @@ -100,8 +107,7 @@ def run_command(cls, args): if source in ["twbx", "twb"]: # TSC's workbook publish path requires ConnectionItem.server_address whenever # embedded connection credentials are supplied; datasource publish does not. - if (args.db_username or args.oauth_username) and not args.db_server: - Errors.exit_with_error(logger, _("publish.errors.db_server_required")) + PublishCommand._require_db_server_for_workbook(args, str_filename, logger) try: published_item = PublishCommand.publish_workbook_file( args=args, From f2a97fb1cfbaa30bea97f213e07832922900bd4b Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 10 Sep 2026 14:43:43 -0700 Subject: [PATCH 5/9] Comment on what test_wb_publish_embedded actually asserts (M3) The e2e test only proves the publish call did not crash. It does not verify credentials actually embedded on the server side; a mismatched --db-server would exit 0 while silently dropping the creds at TSC's request-factory boundary. Document the limitation so future readers do not mistake it for full coverage. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/e2e/online_tests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index b8ea4bfa..cedf1daf 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -475,6 +475,11 @@ def test_view_get_png(self): @pytest.mark.order(11) def test_wb_publish_embedded(self): + # NOTE: This asserts only that publish did not crash. It does NOT verify + # that credentials actually embedded on the server side - a mismatched + # --db-server would still exit 0 while silently dropping the creds at + # TSC's request-factory boundary (W-23855612). A stronger assertion would + # populate connections post-publish and check embed_password=True. file = os.path.join("tests", "assets", TestAssets.TWB_FILE_WITH_EMBEDDED_CONNECTION) name_on_server = TestAssets.get_publishable_name(TestAssets.TWB_FILE_WITH_EMBEDDED_CONNECTION) arguments = TabcmdCall._publish_args(file, name_on_server) From 2a540682d76302baa971599915d2e2a71519eb55 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 10 Sep 2026 14:43:58 -0700 Subject: [PATCH 6/9] Extend --db-server help text with silent-drop caveat (M4) Callers who pass --db-username without --db-server now get a friendly error. But callers who pass a mismatched --db-server still hit the silent-drop bug at the TSC boundary. Document the mismatch footgun in the help text so users notice before they publish. Co-Authored-By: Claude Opus 4.7 (1M context) --- tabcmd/locales/en/tabcmd_messages_en.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index 2624e726..88c40483 100644 --- a/tabcmd/locales/en/tabcmd_messages_en.properties +++ b/tabcmd/locales/en/tabcmd_messages_en.properties @@ -78,7 +78,7 @@ publish.errors.server_resource_not_found=The resource you specified does not exi publish.errors.db_server_required=--db-server is required when publishing a workbook with --db-username or --oauth-username. Use --db-server to specify the database server address the embedded credentials should be associated with. publish.options.append=Append extract file to existing data source publish.options.db-password=Database password for all data sources -publish.options.db-server=Server address of the database to associate with the embedded connection credentials. Required when using --db-username or --oauth-username to publish a workbook. +publish.options.db-server=Server address of the database to associate with the embedded connection credentials. Required when using --db-username or --oauth-username to publish a workbook. Must match the connection server stored in the workbook; a mismatch silently drops the embedded credentials. publish.options.db-username=Database username for all data sources publish.options.encrypt_extracts=Encrypt extracts in the workbook or datasource being published to the server. publish.options.name=Workbook or data source name on the server. If omitted, the workbook or data source will be named after the file name, without the twb(x), tds(x), or tde extension. Publishing a .tde file will create a data source From df9f6c821887f91c0083fabbcd59b3d6884a2915 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 10 Sep 2026 14:44:12 -0700 Subject: [PATCH 7/9] Fix Portuguese grammar on publish.options.db-server Two small corrections in the pt locale: - "banco de dado" -> "banco de dados" (plural is the standard form) - "associado as credenciais" -> "associado as credenciais" (crase, "a"+"as") Co-Authored-By: Claude Opus 4.7 (1M context) --- tabcmd/locales/pt/tabcmd_messages_pt.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/locales/pt/tabcmd_messages_pt.properties b/tabcmd/locales/pt/tabcmd_messages_pt.properties index c9dbfddd..ffccf710 100644 --- a/tabcmd/locales/pt/tabcmd_messages_pt.properties +++ b/tabcmd/locales/pt/tabcmd_messages_pt.properties @@ -77,7 +77,7 @@ publish.errors.unexpected_server_response=Resposta inesperada do servidor: {0} publish.errors.server_resource_not_found=O recurso especificado não existe ou você não tem permissão para vê-lo. Verifique o nome e as permissões do projeto publish.options.append=Anexe o arquivo de extração à fonte de dados existente publish.options.db-password=Senha do banco de dados para todas as fontes de dados -publish.options.db-server=Endereço do servidor do banco de dado associado as credenciais. Obrigatório ao usar --db-username ou --oauth-username. +publish.options.db-server=Endereço do servidor do banco de dados associado às credenciais. Obrigatório ao usar --db-username ou --oauth-username. publish.options.db-username=Nome de usuário do banco de dados para todas as fontes de dados publish.options.encrypt_extracts=Criptografar extrações no servidor publish.options.name=O nome da pasta de trabalho/fonte de dados no servidor. Se omitido, a pasta de trabalho/fonte de dados será nomeado de acordo com nome de arquivo, sem a extensão twb(x), tds(x) ou tde. Publicar um arquivo .tde criará uma fonte de dados From fd661c2530466287003e7d2af9a0f6de966386d3 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 10 Sep 2026 14:45:32 -0700 Subject: [PATCH 8/9] Regenerate .mo files after help-text updates Re-runs doit localize so en (M4 caveat added) and pt (grammar fixed) message catalogs match their .properties sources. Co-Authored-By: Claude Opus 4.7 (1M context) --- tabcmd/locales/en/LC_MESSAGES/tabcmd.mo | Bin 22763 -> 23120 bytes tabcmd/locales/pt/LC_MESSAGES/tabcmd.mo | Bin 18896 -> 18898 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tabcmd/locales/en/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/en/LC_MESSAGES/tabcmd.mo index 7238ce249e974c5f16f0720ce49c643d155464fa..85c42d08ca58183281b31878b73bce0931b545ab 100644 GIT binary patch delta 4475 zcmZ|ReQ?yp9mnxKjXWoi1o8xFHi-m6AfR9(Bxr;LF$$pyls;H;mpjPekqfyCkG6*$ zS_cWXl}E=1Vy7|`M$rc3VH7(Rr6WT?#!l-X1tq{tD@7?1QS9{n$#0qVpJp!i+TGvp zxBK1i?r)QSyzV=($Cvn9TGA%N&%gN{#_xbWs{Qxp#z13osD6ulOx7S{@^K^%!CL$d z)}zG)W@5W@KMtmT0#ork>bY*r<#v-ytEHg?OYk97hs&`5x47#Ekq68vOvCG#hXbf+ zfYCS#??X)_f)>}Jp4;i#|Aahh&SO5~n{IbQcDmPbITmw6Eh?ZmYR2uDfqz5|^buxZ z5~C+$E^43>wD^E?k!xRv3}t?Y3j9qR%=qSG3X1p(RAedSM*~{a15rT zRH_fU`bpFhT|zzoEh^wVzJ_BN7UGXk1Ru=hZADB5bsKW)QnLUo$16yz?zJWz} z5|!fXs1#;0n(F0faSm$FEJaP|SycZ=Q2qSX)vqG4nA>v5KYf{8MjeY~$g-LRs1&Y5 zrT%%;o_H6Pk;|wKd+|kIrT|s1ZP_Q&~BYm1I7DfY% zN3B&YYDP_{)cqQ@#_eeFHPn3{peFJq@-Zo-MeQZdI@E+>XmJzrF^M-^;X~9KU%{T# z5Vq>MsKCad#RpMK5kYmh0r{A{d}+WBQEUAbD&StMliG)%GCL6!;0z=q3A4y6m^G*w zyy)DAitq$#Z~POLnI!9_b|@;)amXbz9rb)8YR#WO1^5i=t=faS?>*FnE?|b| zeUpV+qY0=RX5#G_cI{822Hb%b|KQp`L2b5ga5NUPUg~Eqs^4bRMA}gO?Q|Z;VT^CC zQqcREN}iO;a-=Ua7uCUXREHbg^;c0TJci21-%&I4(YpdqNA**RJ*h@*;zc+BH=r`q zjtQmYbqY%H2dD=xqEgw5sMKK|Dv(LoGc#9jcD6Zppq@L7y6-e9W8JPkkd34Mik;O( z{!~>=dfQuTcY~aY|{R64XR$Ts?%kKYba|^K(qNvB_|+VnL5=a2ez+g|;yc(M zk6~~82$S#=?1LAO3?gp+s>W*k0j@>OybIX|#y8dLxD2bP z*Wu6bSEwaR=i$5Y5zNL!8wI6$J1WI{-G$RQlzO)_op(!-kHrVD9-qRUsF~a~jqKn| z)MkDLbsn_4`YzOx9l%2D#PxdruTanoRHL3(BQStJ!6)!6 z=3{`UbpIOci@UKH51N;A? zWoK35J*fLuIbT8zd<1jwB64JyTV{GoT81Ri{1`KEJL&*>3ln+`Iw>esUDyXx>UbD4 zF$r@}naRUjaX4z`#i)!-!;x5zDYzB~;#O1!c3@xJjs5U|Yd>5^{uRkl8jA2^OvYPh zc{52xWneUN$;?8MVcMOSP^q8vUGJQD6cyOxn2yilK70i$FvO`-g)gCA$4?(7|Jrof zk9hCt9jFxl0(J1bgC)8VTQOy}cNDKdb#NJ#`XY9w0-S~lU^BL02P(5gbG`m%;aKWX zSKpSP@EaNq;iKsPp0^j?Mg?#REq;y4%+PsWy%>j3uSE@9k2Uxjmg6-{#nSJ4nW(}{ z>a$Uqh&b&I3Yu{jYM177s$^rCP8WYLxZ#Vmtd2f|mh?*t2Ij{?ElWc!^IJkoTEi`& zU`IG-L4Ns|F~PtX?}`n_?9GdIS!1ZlHn#>M;aFq1X`%JoWzm+$0@3JWc3C*yxMAGn zf=H>I6ARh@?I9kuvF1=ic)Kkg6sxyKDaL@l9Mtmj&2aUc{7hJv<%SVB$l zus;&BvBqd?BxnO6>yO2v4Pk$r8#G>NM`y)zzJ#688jIV-{&+)UPbUr0rlwFsJREJh zIYcZTr9T^P>bZV1<_h9p9FBPp#=;TCi>$E0mT2?;52n;q-weN%_?!HTLmjIoHxK;} DXvI2- delta 4143 zcmYM$dvwor9LMqZ7Hy_!ejB@4zGHqPxh%JtMN313RU;P3EM-S3m1y+Mx`?b?PZyUQ zoi0*3Y_cOd458E_`@@l27t&I6ILM`*FW=8O?fkaK=kxh}zn{;WX5J z@3`@570hM4n&co)o4z`7Ik6ph8wMBIBD-Kh1RKwlG7(CC2gqcZyeGw~|&u`m}YklRrG0%UG`#Es8z&ckHJ-$Qb+ z&8Q0PLsh6Ao8U#v#popJ&-PnBS82lGsLUp!&cG~8#SbuyHK-DwM3pd#)zr^K5A#uH zW;`mP*{Jz9pyt`-`oADC*tult&s-MEs+};5Y^M!JmAn*n78as1-;A1QKQf1%cKsLz zJ^Eo(=KWC#j6z*khFWm3^HcQb@9=3bhaE>P5W&W1kCITCowWAKmyxXBy>6XFLx*9EYmdOHO|!4ejj~RD?gcemyF%tLR}e>1YdbP!oss478Uq0)ODBfTB%u8hd3K=z_F+bO+r<~M_(m=mxgX!kJ^g;s0mM@0*U7B zp+GY5T4v57=M>a^3sBdsMpbNw>mNeRcitJ|An8nW2vdKLMjivos2B;}reR}Th6-#I zYQkNpg^r>Yx`@gop0}0yT~OB#Mh_>V4tE7AfEB1M`2w}xflTVJlAmTknT4pAF6@Y! z@Gj>#H$KDl-$Dhn4vE3OMlF2C_2YPHXrUg+?%FWad{eO_EiVxUn-`sG}OZVQI&ZZb=}Kud<81=FWtCbM?;aFM(s%q zM|la}gnHaIB5PPJ=HNf5E$T+pG;Jit;49b&=VN1Bf>HP(Ho(s?8mmwh+l+jC`-Q*X zFc6|H3gj?8h8M60j_19sK$fE_P>DLl2asK}-!UIUoz42-2-F#Q3$^!aF&4L@D!UIo zJclWI{+nk7k4YBlh9XoY9z&IGCf{{c+GH9e`nBHqbBGyDk^;RV#=6h)dUT^RNK5KO=l4B<4?n{FRce!FUYy3Z9BEYMwh$0TlJ3{(AnOVt_SmF_vL1@+jM&+~EI!Poqk?0$1Wr z*Dt*-*qZrR$M_K}#PyVcB3caP(pkwvEjSr$dH{xJzgDn2K653h3fZ1ZB-$*#tB$~71#n(`(1m3`j$;Yr?UWW z#^+EMypO}M28%I?6F3vgFdeU;u5URYc<~HG%~Ob4_<2mjg{Z{VqAL6?MqvtBHPrLp ziiQqdTlBC8s^lY3m05;uaRVx_qu2_sU`zA{29M+Ics>0xQ~>4Z;hRVfR*gE0|Dpnl zAH-Q;ed|O+kIgVtsTQG!)yO%s1K0+e6TZwvEnJK#SdP3RY#C;1phhhf~r7YY=i?b z1`FNz7>uJo9>e(T5b7UGV>tuLai2XaFW%}!Id}?N8@Fmh7Lo~@Zf?e=+R${s?6uEza5*={}r|H zCFDu6LH7s$*v!YK^mk)C9>hfa3ss4z5rKVC3Hz9UahqspPxqpYEh$+T5ua3<6!&CI U6JTk--)RFu6KFrf36M5q5uE@ diff --git a/tabcmd/locales/pt/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/pt/LC_MESSAGES/tabcmd.mo index 8929f104fbb80f43797ea57afc70272e3cb6ebac..c50a872123a84149445af5b73fb3b33dd40d9707 100644 GIT binary patch delta 760 zcmXZaOGs346vy$Opu!-S;*^6qIi@5&vchNiXiP^N2qQ#=L}G9vl5mgX!HyGfOgRVTB(Y$A?H!G!*eFFpobNZ{QqG zcYF7g_0TrPgJ>=Kh$(wsO9aDAL?3$}T1M;XSF}?9MB1f)XbF`@C7yx$F@&>NifgD? hJf7Uo4XmH$>{jGDp_t<&MqbC_iO~1O^>p3I)pKSGX}JIZ delta 758 zcmXZaOGs2<7{>8Oj3f-ZsSS>o(WDW(Sd4ea8XI-0g(4c%B4U^kET};SFQ9NXx=?fx z0|nw@kX$JuxCnwaZd@oKE?lW#dD)4(T>5`Ku72QvA*k;2nWHq=AgpqsIhs~JA4%|a);8CO7KmzZup2GWf9~R@<<@KY>btZ?0*e}C0 z#_$$i2|ChEiu@o8^y3YDh`AWVBJ9EiOyFJg(p5eNuo7RRH86lhIEyj-g(Fz}j5Kiu zA7I6EQotUE;wr@?T6AC0i>v6tJ?zB)ScZ|1#7F59+H><*f{R#y8^|HseDi6uoP66ZnSftbgNo zoM?B?DUC=~tlyxGXdLJ4_xeHco{h*$_eJw)GyRDc^*S;x{X=W0%M3*nTUOi#uL8i%HP0g-Z_CwXOaK_ From ac40b1eeda98e89e56bba9c76e5ed4ae1e9dd8d1 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 10 Sep 2026 14:59:33 -0700 Subject: [PATCH 9/9] Drop internal work-item reference from public test comment Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/e2e/online_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index cedf1daf..ed631e0a 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -478,8 +478,8 @@ def test_wb_publish_embedded(self): # NOTE: This asserts only that publish did not crash. It does NOT verify # that credentials actually embedded on the server side - a mismatched # --db-server would still exit 0 while silently dropping the creds at - # TSC's request-factory boundary (W-23855612). A stronger assertion would - # populate connections post-publish and check embed_password=True. + # TSC's request-factory boundary. A stronger assertion would populate + # connections post-publish and check embed_password=True. file = os.path.join("tests", "assets", TestAssets.TWB_FILE_WITH_EMBEDDED_CONNECTION) name_on_server = TestAssets.get_publishable_name(TestAssets.TWB_FILE_WITH_EMBEDDED_CONNECTION) arguments = TabcmdCall._publish_args(file, name_on_server)