From cc88d17fc1e920fba61312fc1ad9aef6d4f9b84e Mon Sep 17 00:00:00 2001 From: Sean Reidy Date: Wed, 6 May 2026 10:07:58 -0400 Subject: [PATCH 1/8] warn when NominalClient is created without a workspace RID Tenants with multiple workspaces and no default workspace set will fail when operations require a workspace. This warning surfaces that early. Co-Authored-By: Claude Sonnet 4.6 --- nominal/core/client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nominal/core/client.py b/nominal/core/client.py index fb0b2c1c..c93f8e54 100644 --- a/nominal/core/client.py +++ b/nominal/core/client.py @@ -3,6 +3,7 @@ import enum import logging import uuid +import warnings from dataclasses import dataclass, field from datetime import datetime, timedelta from io import TextIOBase @@ -177,6 +178,12 @@ def from_token( certifi's trust store is used. connect_timeout: Request connection timeout. """ + if workspace_rid is None: + warnings.warn( + "NominalClient will soon require a workspace RID. Any client which doesn't have a workspace RID specified will fail.", + UserWarning, + stacklevel=2, + ) trust_store_path = certifi.where() if trust_store_path is None else trust_store_path timeout_seconds = connect_timeout.total_seconds() if isinstance(connect_timeout, timedelta) else connect_timeout cfg = ServiceConfiguration( From a1c0a57b63030c9494d564ec70b0e4f6c98b1e19 Mon Sep 17 00:00:00 2001 From: Sean Reidy Date: Wed, 6 May 2026 10:15:28 -0400 Subject: [PATCH 2/8] fix ruff E501 line too long in workspace RID warning Co-Authored-By: Claude Sonnet 4.6 --- nominal/core/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nominal/core/client.py b/nominal/core/client.py index c93f8e54..4954d822 100644 --- a/nominal/core/client.py +++ b/nominal/core/client.py @@ -180,7 +180,8 @@ def from_token( """ if workspace_rid is None: warnings.warn( - "NominalClient will soon require a workspace RID. Any client which doesn't have a workspace RID specified will fail.", + "NominalClient will soon require a workspace RID. " + "Any client which doesn't have a workspace RID specified will fail.", UserWarning, stacklevel=2, ) From 531a3836b55dd5677244e068b2cbb067ee8a4f0c Mon Sep 17 00:00:00 2001 From: Sean Reidy Date: Wed, 6 May 2026 10:27:03 -0400 Subject: [PATCH 3/8] pass dummy workspace RID in test_experimental_as_user test Co-Authored-By: Claude Sonnet 4.6 --- tests/core/test_clientsbunch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/test_clientsbunch.py b/tests/core/test_clientsbunch.py index 0f87e8dd..9aef3203 100644 --- a/tests/core/test_clientsbunch.py +++ b/tests/core/test_clientsbunch.py @@ -240,7 +240,7 @@ def test_experimental_as_user_returns_derived_nominal_client(monkeypatch): "https://api.nominal.test", "test-agent", "token", - None, + "ri.workspace.main.workspace.test", ) ) From 859484ad11dd42630598838cd1c927d273ce7d0f Mon Sep 17 00:00:00 2001 From: Sean Reidy Date: Wed, 6 May 2026 10:32:01 -0400 Subject: [PATCH 4/8] suppress UserWarning in e2e tests that intentionally use no workspace RID Co-Authored-By: Claude Sonnet 4.6 --- tests/e2e/test_workspace_resolution.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e/test_workspace_resolution.py b/tests/e2e/test_workspace_resolution.py index 377cfe89..504d5a3a 100644 --- a/tests/e2e/test_workspace_resolution.py +++ b/tests/e2e/test_workspace_resolution.py @@ -43,6 +43,7 @@ def test_workspace_rid_for_search_returns_none_for_all(client: NominalClient) -> assert client._workspace_rid_for_search(WorkspaceSearchType.ALL) is None +@pytest.mark.filterwarnings("ignore::UserWarning") def test_unconfigured_client_uses_workspace_service_default(client: NominalClient, pytestconfig) -> None: """Without a pinned workspace, DEFAULT should resolve to the tenant's service-side default.""" unconfigured_client = _client_with_workspace_override(client, pytestconfig, workspace_rid=None) @@ -52,6 +53,7 @@ def test_unconfigured_client_uses_workspace_service_default(client: NominalClien assert unconfigured_client._workspace_rid_for_search(WorkspaceSearchType.DEFAULT) == expected_workspace_rid +@pytest.mark.filterwarnings("ignore::UserWarning") def test_configured_workspace_rid_takes_precedence_over_service_default(client: NominalClient, pytestconfig) -> None: """A pinned workspace RID should win over the tenant default exposed by the workspace service.""" unconfigured_client = _client_with_workspace_override(client, pytestconfig, workspace_rid=None) From dd0eab9c1fe343e14afe59ff4458cf4fd42f7cfa Mon Sep 17 00:00:00 2001 From: Sean Reidy Date: Mon, 11 May 2026 12:49:54 -0400 Subject: [PATCH 5/8] fix warning stacklevel for from_profile and create entry points Each public entry point now emits the warning directly with stacklevel=2 so it points to user code. from_token skips its own warning when called internally via _workspace_warning_emitted=True. Co-Authored-By: Claude Sonnet 4.6 --- nominal/core/client.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/nominal/core/client.py b/nominal/core/client.py index 4954d822..de234735 100644 --- a/nominal/core/client.py +++ b/nominal/core/client.py @@ -146,6 +146,13 @@ def from_profile( """ config = NominalConfig.from_yaml() prof = config.get_profile(profile) + if prof.workspace_rid is None: + warnings.warn( + "NominalClient will soon require a workspace RID. " + "Any client which doesn't have a workspace RID specified will fail.", + UserWarning, + stacklevel=2, + ) client = cls.from_token( prof.token, prof.base_url, @@ -153,6 +160,7 @@ def from_profile( trust_store_path=trust_store_path, connect_timeout=connect_timeout, _profile=profile, + _workspace_warning_emitted=True, ) return client @@ -166,6 +174,7 @@ def from_token( trust_store_path: str | None = None, connect_timeout: timedelta | float = DEFAULT_CONNECT_TIMEOUT, _profile: str | None = None, + _workspace_warning_emitted: bool = False, ) -> Self: """Create a connection to the Nominal platform from a token. @@ -178,7 +187,7 @@ def from_token( certifi's trust store is used. connect_timeout: Request connection timeout. """ - if workspace_rid is None: + if workspace_rid is None and not _workspace_warning_emitted: warnings.warn( "NominalClient will soon require a workspace RID. " "Any client which doesn't have a workspace RID specified will fail.", @@ -217,12 +226,20 @@ def create( """ if token is None: token = _config.get_token(base_url) + if workspace_rid is None: + warnings.warn( + "NominalClient will soon require a workspace RID. " + "Any client which doesn't have a workspace RID specified will fail.", + UserWarning, + stacklevel=2, + ) return cls.from_token( token, base_url, trust_store_path=trust_store_path, connect_timeout=connect_timeout, workspace_rid=workspace_rid, + _workspace_warning_emitted=True, ) def __repr__(self) -> str: From bb6e865c4ea763d866227007ed8d9bef72c5b3bf Mon Sep 17 00:00:00 2001 From: Sean Reidy Date: Mon, 11 May 2026 13:14:44 -0400 Subject: [PATCH 6/8] ci: retrigger workflows From b9239fd63e13887d9eed1b0459b34dc743e9ceb1 Mon Sep 17 00:00:00 2001 From: Sean Reidy Date: Thu, 14 May 2026 10:57:24 -0400 Subject: [PATCH 7/8] simplify workspace warning to single site in from_token; assert warning in e2e tests All creation paths delegate to from_token, so the warning only needs to live there. Removes _workspace_warning_emitted and the duplicate warnings in create() and from_profile(). E2e tests now assert the warning with pytest.warns rather than ignoring it. Co-Authored-By: Claude Sonnet 4.6 --- nominal/core/client.py | 19 +------------------ tests/e2e/test_workspace_resolution.py | 8 ++++---- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/nominal/core/client.py b/nominal/core/client.py index 5ee6caca..f569c058 100644 --- a/nominal/core/client.py +++ b/nominal/core/client.py @@ -150,13 +150,6 @@ def from_profile( """ config = NominalConfig.from_yaml() prof = config.get_profile(profile) - if prof.workspace_rid is None: - warnings.warn( - "NominalClient will soon require a workspace RID. " - "Any client which doesn't have a workspace RID specified will fail.", - UserWarning, - stacklevel=2, - ) client = cls.from_token( prof.token, prof.base_url, @@ -165,7 +158,6 @@ def from_profile( connect_timeout=connect_timeout, extra_headers=extra_headers, _profile=profile, - _workspace_warning_emitted=True, ) return client @@ -180,7 +172,6 @@ def from_token( connect_timeout: timedelta | float = DEFAULT_CONNECT_TIMEOUT, extra_headers: HeaderProvider | Mapping[str, str] | None = None, _profile: str | None = None, - _workspace_warning_emitted: bool = False, ) -> Self: """Create a connection to the Nominal platform from a token. @@ -194,7 +185,7 @@ def from_token( connect_timeout: Request connection timeout. extra_headers: Extra request headers, either as a mapping or HeaderProvider. """ - if workspace_rid is None and not _workspace_warning_emitted: + if workspace_rid is None: warnings.warn( "NominalClient will soon require a workspace RID. " "Any client which doesn't have a workspace RID specified will fail.", @@ -245,13 +236,6 @@ def create( """ if token is None: token = _config.get_token(base_url) - if workspace_rid is None: - warnings.warn( - "NominalClient will soon require a workspace RID. " - "Any client which doesn't have a workspace RID specified will fail.", - UserWarning, - stacklevel=2, - ) return cls.from_token( token, base_url, @@ -259,7 +243,6 @@ def create( connect_timeout=connect_timeout, workspace_rid=workspace_rid, extra_headers=extra_headers, - _workspace_warning_emitted=True, ) def __repr__(self) -> str: diff --git a/tests/e2e/test_workspace_resolution.py b/tests/e2e/test_workspace_resolution.py index 504d5a3a..2a418100 100644 --- a/tests/e2e/test_workspace_resolution.py +++ b/tests/e2e/test_workspace_resolution.py @@ -43,20 +43,20 @@ def test_workspace_rid_for_search_returns_none_for_all(client: NominalClient) -> assert client._workspace_rid_for_search(WorkspaceSearchType.ALL) is None -@pytest.mark.filterwarnings("ignore::UserWarning") def test_unconfigured_client_uses_workspace_service_default(client: NominalClient, pytestconfig) -> None: """Without a pinned workspace, DEFAULT should resolve to the tenant's service-side default.""" - unconfigured_client = _client_with_workspace_override(client, pytestconfig, workspace_rid=None) + with pytest.warns(UserWarning, match="NominalClient will soon require a workspace RID"): + unconfigured_client = _client_with_workspace_override(client, pytestconfig, workspace_rid=None) expected_workspace_rid = _get_service_default_workspace_rid(unconfigured_client) assert unconfigured_client._clients.resolve_default_workspace_rid() == expected_workspace_rid assert unconfigured_client._workspace_rid_for_search(WorkspaceSearchType.DEFAULT) == expected_workspace_rid -@pytest.mark.filterwarnings("ignore::UserWarning") def test_configured_workspace_rid_takes_precedence_over_service_default(client: NominalClient, pytestconfig) -> None: """A pinned workspace RID should win over the tenant default exposed by the workspace service.""" - unconfigured_client = _client_with_workspace_override(client, pytestconfig, workspace_rid=None) + with pytest.warns(UserWarning, match="NominalClient will soon require a workspace RID"): + unconfigured_client = _client_with_workspace_override(client, pytestconfig, workspace_rid=None) service_default_workspace_rid = _get_service_default_workspace_rid(unconfigured_client) configured_workspace = next( ( From cd2b1f250f33a169fbad2bb9b3b106dda006c365 Mon Sep 17 00:00:00 2001 From: Sean Reidy Date: Mon, 18 May 2026 17:05:16 -0400 Subject: [PATCH 8/8] fix spurious workspace warning when as_user() derives a client as_user() calls from_token() internally and should not re-warn the user since the warning was already emitted when the original client was created. Co-Authored-By: Claude Sonnet 4.6 --- nominal/core/client.py | 3 ++- nominal/experimental/impersonation/__init__.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nominal/core/client.py b/nominal/core/client.py index f569c058..0303a832 100644 --- a/nominal/core/client.py +++ b/nominal/core/client.py @@ -172,6 +172,7 @@ def from_token( connect_timeout: timedelta | float = DEFAULT_CONNECT_TIMEOUT, extra_headers: HeaderProvider | Mapping[str, str] | None = None, _profile: str | None = None, + _workspace_warning_emitted: bool = False, ) -> Self: """Create a connection to the Nominal platform from a token. @@ -185,7 +186,7 @@ def from_token( connect_timeout: Request connection timeout. extra_headers: Extra request headers, either as a mapping or HeaderProvider. """ - if workspace_rid is None: + if workspace_rid is None and not _workspace_warning_emitted: warnings.warn( "NominalClient will soon require a workspace RID. " "Any client which doesn't have a workspace RID specified will fail.", diff --git a/nominal/experimental/impersonation/__init__.py b/nominal/experimental/impersonation/__init__.py index dd2c41a2..a0b66fe6 100644 --- a/nominal/experimental/impersonation/__init__.py +++ b/nominal/experimental/impersonation/__init__.py @@ -18,4 +18,5 @@ def as_user(client: NominalClient, user_rid: str) -> NominalClient: connect_timeout=client._clients._service_config.connect_timeout, extra_headers={ON_BEHALF_OF_USER_RID_HEADER: user_rid}, _profile=client._profile, + _workspace_warning_emitted=True, )