Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- The `CONNECT_SERVER_VERSION` environment variable can now be set to tell
rsconnect-python which Connect version to assume for feature-availability
checks. Some servers can be configured to suppress their version, which makes
version-gated features (such as draft deploys and git metadata) default to
off; setting `CONNECT_SERVER_VERSION` opts back in. When it is set, its value
is used directly and the `server_settings` request for the version is skipped.
- `rsconnect deploy` commands now verify content before activating it. The new
bundle is deployed as a draft, its preview URL is accessed to confirm the
content starts, and only then is the bundle activated. If verification fails,
Expand Down
20 changes: 19 additions & 1 deletion rsconnect/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,24 @@ def server_settings(self) -> ServerSettings:
response = self._server.handle_bad_response(response)
return response

def server_version(self) -> str:
"""
Determine the Connect server version used for feature-availability checks.

A server can be configured to suppress its version from the
``server_settings`` endpoint, which makes version-gated features default to
"off". Setting the ``CONNECT_SERVER_VERSION`` environment variable overrides
this: when it is set we use it and skip the ``server_settings`` request
entirely, so the library acts as if it is talking to that version.

:return: The server version string, or an empty string if it is unknown.
"""
env_version = os.environ.get("CONNECT_SERVER_VERSION")
if env_version:
logger.debug(f"Using CONNECT_SERVER_VERSION={env_version} for server version checks")
return env_version
return self.server_settings().get("version", "")

def python_settings(self) -> PyInfo:
response = cast(Union[PyInfo, HTTPResponse], self.get("v1/server_settings/python"))
response = self._server.handle_bad_response(response)
Expand Down Expand Up @@ -1788,7 +1806,7 @@ def supports_verify_before_activate(self) -> bool:
return False
if self._draft_deploy_supported is None:
try:
server_version = self.client.server_settings().get("version", "")
server_version = self.client.server_version()
except Exception:
server_version = None
self._draft_deploy_supported = server_supports_draft_deploy(server_version)
Expand Down
20 changes: 10 additions & 10 deletions rsconnect/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1549,7 +1549,7 @@ def deploy_notebook(
# Prepare metadata for upload
server_version = None
if isinstance(ce.client, RSConnectClient):
server_version = ce.client.server_settings().get("version", "")
server_version = ce.client.server_version()
deploy_metadata = prepare_deploy_metadata(base_dir, metadata, no_metadata, server_version)
ce.metadata = deploy_metadata

Expand Down Expand Up @@ -1727,7 +1727,7 @@ def deploy_voila(
# Prepare metadata for upload
server_version = None
if isinstance(ce.client, RSConnectClient):
server_version = ce.client.server_settings().get("version", "")
server_version = ce.client.server_version()
base_dir = path if isdir(path) else dirname(path)
deploy_metadata = prepare_deploy_metadata(base_dir, metadata, no_metadata, server_version)
ce.metadata = deploy_metadata
Expand Down Expand Up @@ -1825,7 +1825,7 @@ def deploy_manifest(
# Prepare metadata for upload
server_version = None
if isinstance(ce.client, RSConnectClient):
server_version = ce.client.server_settings().get("version", "")
server_version = ce.client.server_version()
base_dir = dirname(file_name)
deploy_metadata = prepare_deploy_metadata(base_dir, metadata, no_metadata, server_version)
ce.metadata = deploy_metadata
Expand Down Expand Up @@ -1920,7 +1920,7 @@ def deploy_bundle(
# explicit --metadata overrides are sent.
server_version = None
if isinstance(ce.client, RSConnectClient):
server_version = ce.client.server_settings().get("version", "")
server_version = ce.client.server_version()
ce.metadata = prepare_deploy_metadata(None, metadata, no_metadata, server_version)

(
Expand Down Expand Up @@ -2134,7 +2134,7 @@ def quickstart_hint() -> str:

server_version = None
if isinstance(ce.client, RSConnectClient):
server_version = ce.client.server_settings().get("version", "")
server_version = ce.client.server_version()
ce.metadata = prepare_deploy_metadata(directory, metadata, no_metadata, server_version)

(
Expand Down Expand Up @@ -2406,7 +2406,7 @@ def deploy_quarto(
# Prepare metadata for upload
server_version = None
if isinstance(ce.client, RSConnectClient):
server_version = ce.client.server_settings().get("version", "")
server_version = ce.client.server_version()
deploy_metadata = prepare_deploy_metadata(base_dir, metadata, no_metadata, server_version)
ce.metadata = deploy_metadata

Expand Down Expand Up @@ -2521,7 +2521,7 @@ def deploy_tensorflow(
# Prepare metadata for upload
server_version = None
if isinstance(ce.client, RSConnectClient):
server_version = ce.client.server_settings().get("version", "")
server_version = ce.client.server_version()
deploy_metadata = prepare_deploy_metadata(directory, metadata, no_metadata, server_version)
ce.metadata = deploy_metadata

Expand Down Expand Up @@ -2647,7 +2647,7 @@ def deploy_html(
# Prepare metadata for upload
server_version = None
if isinstance(ce.client, RSConnectClient):
server_version = ce.client.server_settings().get("version", "")
server_version = ce.client.server_version()
base_dir = path if isdir(path) else dirname(path)
deploy_metadata = prepare_deploy_metadata(base_dir, metadata, no_metadata, server_version)
ce.metadata = deploy_metadata
Expand Down Expand Up @@ -2866,7 +2866,7 @@ def deploy_app(
# Update the starlette version if needed. After all users are on Connect
# 2024.01.1 or later, this can be removed. Requires access to the
# Connect server version, which may be hidden.
connect_version_string = ce.client.server_settings().get("version", "")
connect_version_string = ce.client.server_version()
server_version = connect_version_string
if connect_version_string:
environment = fix_starlette_requirements(
Expand Down Expand Up @@ -3044,7 +3044,7 @@ def deploy_nodejs(
)

if isinstance(ce.client, RSConnectClient):
connect_version_string = ce.client.server_settings().get("version", "")
connect_version_string = ce.client.server_version()
server_version = connect_version_string

deploy_metadata = prepare_deploy_metadata(directory, metadata, no_metadata, server_version)
Expand Down
51 changes: 51 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io
import json
import os
import sys
from unittest import TestCase
from unittest.mock import Mock, patch
Expand Down Expand Up @@ -1122,3 +1123,53 @@ def test_deploy_git_requires_repository(self):

with pytest.raises(RSConnectException, match="Repository URL is required"):
RSConnectExecutor.deploy_git(executor)


class RSConnectClientServerVersionTestCase(TestCase):
"""Tests for RSConnectClient.server_version() and the CONNECT_SERVER_VERSION override."""

@httpretty.activate(verbose=True, allow_net_connect=False)
def test_server_version_from_settings(self):
"""Without the env var, the version comes from the server_settings endpoint."""
server = RSConnectServer("http://test-server", "api_key")
client = RSConnectClient(server)
httpretty.register_uri(
httpretty.GET,
"http://test-server/__api__/server_settings",
body=json.dumps({"hostname": "test-server", "version": "2025.06.0"}),
status=200,
forcing_headers={"Content-Type": "application/json"},
)

with patch.dict("os.environ", {}, clear=False):
os.environ.pop("CONNECT_SERVER_VERSION", None)
self.assertEqual(client.server_version(), "2025.06.0")

@httpretty.activate(verbose=True, allow_net_connect=False)
def test_server_version_hidden_returns_empty(self):
"""A suppressed version yields an empty string when the env var is unset."""
server = RSConnectServer("http://test-server", "api_key")
client = RSConnectClient(server)
httpretty.register_uri(
httpretty.GET,
"http://test-server/__api__/server_settings",
body=json.dumps({"hostname": "test-server"}),
status=200,
forcing_headers={"Content-Type": "application/json"},
)

with patch.dict("os.environ", {}, clear=False):
os.environ.pop("CONNECT_SERVER_VERSION", None)
self.assertEqual(client.server_version(), "")

@httpretty.activate(verbose=True, allow_net_connect=False)
def test_server_version_env_var_overrides_without_request(self):
"""When CONNECT_SERVER_VERSION is set, it is returned without hitting the server."""
server = RSConnectServer("http://test-server", "api_key")
client = RSConnectClient(server)

with patch.dict("os.environ", {"CONNECT_SERVER_VERSION": "2025.12.0"}):
self.assertEqual(client.server_version(), "2025.12.0")

# No request to server_settings should have been made.
self.assertFalse(httpretty.has_request())
Loading