Skip to content

Commit 0dbd416

Browse files
Support CONNECT_SERVER_VERSION env var for feature checks (#811)
Some Connect servers can be configured to suppress their version from the server_settings endpoint, which makes version-gated features (draft deploys, git metadata) default to off. Add a centralized RSConnectClient.server_version() that returns CONNECT_SERVER_VERSION when set (skipping the server_settings request) and otherwise falls back to the reported version. Route all version lookups through it. Fixes #807 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 75a68b5 commit 0dbd416

4 files changed

Lines changed: 86 additions & 11 deletions

File tree

docs/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- The `CONNECT_SERVER_VERSION` environment variable can now be set to tell
11+
rsconnect-python which Connect version to assume for feature-availability
12+
checks. Some servers can be configured to suppress their version, which makes
13+
version-gated features (such as draft deploys and git metadata) default to
14+
off; setting `CONNECT_SERVER_VERSION` opts back in. When it is set, its value
15+
is used directly and the `server_settings` request for the version is skipped.
1016
- `rsconnect deploy` commands now verify content before activating it. The new
1117
bundle is deployed as a draft, its preview URL is accessed to confirm the
1218
content starts, and only then is the bundle activated. If verification fails,

rsconnect/api.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,24 @@ def server_settings(self) -> ServerSettings:
606606
response = self._server.handle_bad_response(response)
607607
return response
608608

609+
def server_version(self) -> str:
610+
"""
611+
Determine the Connect server version used for feature-availability checks.
612+
613+
A server can be configured to suppress its version from the
614+
``server_settings`` endpoint, which makes version-gated features default to
615+
"off". Setting the ``CONNECT_SERVER_VERSION`` environment variable overrides
616+
this: when it is set we use it and skip the ``server_settings`` request
617+
entirely, so the library acts as if it is talking to that version.
618+
619+
:return: The server version string, or an empty string if it is unknown.
620+
"""
621+
env_version = os.environ.get("CONNECT_SERVER_VERSION")
622+
if env_version:
623+
logger.debug(f"Using CONNECT_SERVER_VERSION={env_version} for server version checks")
624+
return env_version
625+
return self.server_settings().get("version", "")
626+
609627
def python_settings(self) -> PyInfo:
610628
response = cast(Union[PyInfo, HTTPResponse], self.get("v1/server_settings/python"))
611629
response = self._server.handle_bad_response(response)
@@ -1788,7 +1806,7 @@ def supports_verify_before_activate(self) -> bool:
17881806
return False
17891807
if self._draft_deploy_supported is None:
17901808
try:
1791-
server_version = self.client.server_settings().get("version", "")
1809+
server_version = self.client.server_version()
17921810
except Exception:
17931811
server_version = None
17941812
self._draft_deploy_supported = server_supports_draft_deploy(server_version)

rsconnect/main.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,7 @@ def deploy_notebook(
15491549
# Prepare metadata for upload
15501550
server_version = None
15511551
if isinstance(ce.client, RSConnectClient):
1552-
server_version = ce.client.server_settings().get("version", "")
1552+
server_version = ce.client.server_version()
15531553
deploy_metadata = prepare_deploy_metadata(base_dir, metadata, no_metadata, server_version)
15541554
ce.metadata = deploy_metadata
15551555

@@ -1727,7 +1727,7 @@ def deploy_voila(
17271727
# Prepare metadata for upload
17281728
server_version = None
17291729
if isinstance(ce.client, RSConnectClient):
1730-
server_version = ce.client.server_settings().get("version", "")
1730+
server_version = ce.client.server_version()
17311731
base_dir = path if isdir(path) else dirname(path)
17321732
deploy_metadata = prepare_deploy_metadata(base_dir, metadata, no_metadata, server_version)
17331733
ce.metadata = deploy_metadata
@@ -1825,7 +1825,7 @@ def deploy_manifest(
18251825
# Prepare metadata for upload
18261826
server_version = None
18271827
if isinstance(ce.client, RSConnectClient):
1828-
server_version = ce.client.server_settings().get("version", "")
1828+
server_version = ce.client.server_version()
18291829
base_dir = dirname(file_name)
18301830
deploy_metadata = prepare_deploy_metadata(base_dir, metadata, no_metadata, server_version)
18311831
ce.metadata = deploy_metadata
@@ -1920,7 +1920,7 @@ def deploy_bundle(
19201920
# explicit --metadata overrides are sent.
19211921
server_version = None
19221922
if isinstance(ce.client, RSConnectClient):
1923-
server_version = ce.client.server_settings().get("version", "")
1923+
server_version = ce.client.server_version()
19241924
ce.metadata = prepare_deploy_metadata(None, metadata, no_metadata, server_version)
19251925

19261926
(
@@ -2134,7 +2134,7 @@ def quickstart_hint() -> str:
21342134

21352135
server_version = None
21362136
if isinstance(ce.client, RSConnectClient):
2137-
server_version = ce.client.server_settings().get("version", "")
2137+
server_version = ce.client.server_version()
21382138
ce.metadata = prepare_deploy_metadata(directory, metadata, no_metadata, server_version)
21392139

21402140
(
@@ -2406,7 +2406,7 @@ def deploy_quarto(
24062406
# Prepare metadata for upload
24072407
server_version = None
24082408
if isinstance(ce.client, RSConnectClient):
2409-
server_version = ce.client.server_settings().get("version", "")
2409+
server_version = ce.client.server_version()
24102410
deploy_metadata = prepare_deploy_metadata(base_dir, metadata, no_metadata, server_version)
24112411
ce.metadata = deploy_metadata
24122412

@@ -2521,7 +2521,7 @@ def deploy_tensorflow(
25212521
# Prepare metadata for upload
25222522
server_version = None
25232523
if isinstance(ce.client, RSConnectClient):
2524-
server_version = ce.client.server_settings().get("version", "")
2524+
server_version = ce.client.server_version()
25252525
deploy_metadata = prepare_deploy_metadata(directory, metadata, no_metadata, server_version)
25262526
ce.metadata = deploy_metadata
25272527

@@ -2647,7 +2647,7 @@ def deploy_html(
26472647
# Prepare metadata for upload
26482648
server_version = None
26492649
if isinstance(ce.client, RSConnectClient):
2650-
server_version = ce.client.server_settings().get("version", "")
2650+
server_version = ce.client.server_version()
26512651
base_dir = path if isdir(path) else dirname(path)
26522652
deploy_metadata = prepare_deploy_metadata(base_dir, metadata, no_metadata, server_version)
26532653
ce.metadata = deploy_metadata
@@ -2866,7 +2866,7 @@ def deploy_app(
28662866
# Update the starlette version if needed. After all users are on Connect
28672867
# 2024.01.1 or later, this can be removed. Requires access to the
28682868
# Connect server version, which may be hidden.
2869-
connect_version_string = ce.client.server_settings().get("version", "")
2869+
connect_version_string = ce.client.server_version()
28702870
server_version = connect_version_string
28712871
if connect_version_string:
28722872
environment = fix_starlette_requirements(
@@ -3044,7 +3044,7 @@ def deploy_nodejs(
30443044
)
30453045

30463046
if isinstance(ce.client, RSConnectClient):
3047-
connect_version_string = ce.client.server_settings().get("version", "")
3047+
connect_version_string = ce.client.server_version()
30483048
server_version = connect_version_string
30493049

30503050
deploy_metadata = prepare_deploy_metadata(directory, metadata, no_metadata, server_version)

tests/test_api.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import io
22
import json
3+
import os
34
import sys
45
from unittest import TestCase
56
from unittest.mock import Mock, patch
@@ -1122,3 +1123,53 @@ def test_deploy_git_requires_repository(self):
11221123

11231124
with pytest.raises(RSConnectException, match="Repository URL is required"):
11241125
RSConnectExecutor.deploy_git(executor)
1126+
1127+
1128+
class RSConnectClientServerVersionTestCase(TestCase):
1129+
"""Tests for RSConnectClient.server_version() and the CONNECT_SERVER_VERSION override."""
1130+
1131+
@httpretty.activate(verbose=True, allow_net_connect=False)
1132+
def test_server_version_from_settings(self):
1133+
"""Without the env var, the version comes from the server_settings endpoint."""
1134+
server = RSConnectServer("http://test-server", "api_key")
1135+
client = RSConnectClient(server)
1136+
httpretty.register_uri(
1137+
httpretty.GET,
1138+
"http://test-server/__api__/server_settings",
1139+
body=json.dumps({"hostname": "test-server", "version": "2025.06.0"}),
1140+
status=200,
1141+
forcing_headers={"Content-Type": "application/json"},
1142+
)
1143+
1144+
with patch.dict("os.environ", {}, clear=False):
1145+
os.environ.pop("CONNECT_SERVER_VERSION", None)
1146+
self.assertEqual(client.server_version(), "2025.06.0")
1147+
1148+
@httpretty.activate(verbose=True, allow_net_connect=False)
1149+
def test_server_version_hidden_returns_empty(self):
1150+
"""A suppressed version yields an empty string when the env var is unset."""
1151+
server = RSConnectServer("http://test-server", "api_key")
1152+
client = RSConnectClient(server)
1153+
httpretty.register_uri(
1154+
httpretty.GET,
1155+
"http://test-server/__api__/server_settings",
1156+
body=json.dumps({"hostname": "test-server"}),
1157+
status=200,
1158+
forcing_headers={"Content-Type": "application/json"},
1159+
)
1160+
1161+
with patch.dict("os.environ", {}, clear=False):
1162+
os.environ.pop("CONNECT_SERVER_VERSION", None)
1163+
self.assertEqual(client.server_version(), "")
1164+
1165+
@httpretty.activate(verbose=True, allow_net_connect=False)
1166+
def test_server_version_env_var_overrides_without_request(self):
1167+
"""When CONNECT_SERVER_VERSION is set, it is returned without hitting the server."""
1168+
server = RSConnectServer("http://test-server", "api_key")
1169+
client = RSConnectClient(server)
1170+
1171+
with patch.dict("os.environ", {"CONNECT_SERVER_VERSION": "2025.12.0"}):
1172+
self.assertEqual(client.server_version(), "2025.12.0")
1173+
1174+
# No request to server_settings should have been made.
1175+
self.assertFalse(httpretty.has_request())

0 commit comments

Comments
 (0)