Skip to content

feat: Add Posit product environment check helpers #391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/posit/connect/_utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

import os
import warnings

from typing_extensions import Any

from ..environment import is_local as env_is_local


def update_dict_values(obj: dict[str, Any], /, **kwargs: Any) -> None:
"""
Expand Down Expand Up @@ -33,10 +35,15 @@ def update_dict_values(obj: dict[str, Any], /, **kwargs: Any) -> None:


def is_local() -> bool:
"""Returns true if called from a piece of content running on a Connect server.
"""
Check if code is running locally.

The connect server will always set the environment variable `RSTUDIO_PRODUCT=CONNECT`.
We can use this environment variable to determine if the content is running locally
or on a Connect server.
.. deprecated:: 0.9.0
Use :func:`posit.environment.is_local` instead.
"""
return os.getenv("RSTUDIO_PRODUCT") != "CONNECT"
warnings.warn(
"posit.connect._utils.is_local is deprecated. Use posit.environment.is_local instead.",
DeprecationWarning,
stacklevel=2,
)
return env_is_local()
8 changes: 5 additions & 3 deletions src/posit/connect/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ def with_user_session_token(self, token: str) -> Client:
--------
```python
from posit.connect import Client

client = Client().with_user_session_token("my-user-session-token")
```

Expand All @@ -218,13 +219,14 @@ def with_user_session_token(self, token: str) -> Client:

client = Client()


@reactive.calc
def visitor_client():
## read the user session token and generate a new client
user_session_token = session.http_conn.headers.get(
"Posit-Connect-User-Session-Token"
)
user_session_token = session.http_conn.headers.get("Posit-Connect-User-Session-Token")
return client.with_user_session_token(user_session_token)


@render.text
def user_profile():
# fetch the viewer's profile information
Expand Down
2 changes: 1 addition & 1 deletion src/posit/connect/external/databricks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import requests
from typing_extensions import Callable, Dict, Optional

from .._utils import is_local
from ...environment import is_local
from ..client import Client
from ..oauth import Credentials

Expand Down
2 changes: 1 addition & 1 deletion src/posit/connect/external/snowflake.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from typing_extensions import Optional

from .._utils import is_local
from ...environment import is_local
from ..client import Client


Expand Down
30 changes: 30 additions & 0 deletions src/posit/environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from __future__ import annotations

import os


def get_product() -> str | None:
"""Returns the product name if called with a Posit product.

The products will always set the environment variable `POSIT_PRODUCT=<product-name>`
or `RSTUDIO_PRODUCT=<product-name>`.

RSTUDIO_PRODUCT is deprecated and acts as a fallback for backwards compatibility.
It is recommended to use POSIT_PRODUCT instead.
"""
return os.getenv("POSIT_PRODUCT") or os.getenv("RSTUDIO_PRODUCT")


def is_local() -> bool:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose is_local is a little subjective since their application could run elsewhere that isn't their machine or Posit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like your suggestion above. will leave deprecation warning in utils is_local but can map it use the global vars.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

going to keep this since staying with functional approach

"""Returns true if called while running locally."""
return get_product() is None


def is_running_on_connect() -> bool:
"""Returns true if called from a piece of content running on a Connect server."""
return get_product() == "CONNECT"


def is_running_on_workbench() -> bool:
"""Returns true if called from within a Workbench server."""
return get_product() == "WORKBENCH"
Comment on lines +1 to +30
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to support dynamic environment variables here? Another option is to use static variables.

from __future__ import annotations

import os

POSIT_PRODUCT = os.getenv("POSIT_PRODUCT") or os.getenv("RSTUDIO_PRODUCT")
IS_POSIT_CONNECT = POSIT_PRODUCT == "CONNECT"
IS_POSIT_WORKBENCH = POSIT_PRODUCT == "WORKBENCH"

Then from external.databricks/snowflake...

from .. import runtime

if not runtime.POSIT_PRODUCT:
    return;

Feel free to disregard this if you have already thought through it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is great feedback. did not consider this.

i think i could go either way here. your approach is definitely simpler. let me ensure its testable. i would think just importing the module should trigger this to run but can check.

The connectapi and connectcreds equivalents are functions so followed suit but i like simple :)

Copy link
Collaborator

@dbkegley dbkegley Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be a little misleading for users today since workbench doesn't support this var yet.

I haven't tested this on workbench yet but I'm expecting to do something like this:

def is_workbench() -> bool:
"""Attempts to return true if called from a piece of content running on Posit Workbench.
There is not yet a definitive way to determine if the content is running on Workbench. This method is best-effort.
"""
return (
"RSW_LAUNCHER" in os.environ
or "RSTUDIO_MULTI_SESSION" in os.environ
or "RS_SERVER_ADDRESS" in os.environ
or "RS_SERVER_URL" in os.environ
)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the functional approach is going to best here, especially from a testing perspective. Complicates things when the values are set upon import. Going to keep it functional.

@dbkegley yeah that is fine! can add these checks into the workbench runtime check

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@atheriel, do you have any thoughts here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always the option punt and not have any workbench related check for now.

6 changes: 3 additions & 3 deletions tests/posit/connect/external/test_databricks.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def test_new_bearer_authorization_header(self):
def test_get_auth_type_local(self):
assert _get_auth_type("local-auth") == "local-auth"

@patch.dict("os.environ", {"RSTUDIO_PRODUCT": "CONNECT"})
@patch.dict("os.environ", {"POSIT_PRODUCT": "CONNECT"})
def test_get_auth_type_connect(self):
assert _get_auth_type("local-auth") == POSIT_OAUTH_INTEGRATION_AUTH_TYPE

Expand Down Expand Up @@ -176,7 +176,7 @@ def test_local_content_credentials_strategy(self):

@patch.dict("os.environ", {"CONNECT_CONTENT_SESSION_TOKEN": "cit"})
@responses.activate
@patch.dict("os.environ", {"RSTUDIO_PRODUCT": "CONNECT"})
@patch.dict("os.environ", {"POSIT_PRODUCT": "CONNECT"})
def test_posit_content_credentials_strategy(self):
register_mocks()

Expand All @@ -191,7 +191,7 @@ def test_posit_content_credentials_strategy(self):
assert cp() == {"Authorization": "Bearer content-access-token"}

@responses.activate
@patch.dict("os.environ", {"RSTUDIO_PRODUCT": "CONNECT"})
@patch.dict("os.environ", {"POSIT_PRODUCT": "CONNECT"})
def test_posit_credentials_strategy(self):
register_mocks()

Expand Down
2 changes: 1 addition & 1 deletion tests/posit/connect/external/test_snowflake.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def register_mocks():

class TestPositAuthenticator:
@responses.activate
@patch.dict("os.environ", {"RSTUDIO_PRODUCT": "CONNECT"})
@patch.dict("os.environ", {"POSIT_PRODUCT": "CONNECT"})
def test_posit_authenticator(self):
register_mocks()

Expand Down
6 changes: 3 additions & 3 deletions tests/posit/connect/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def test_init(
MockSession.assert_called_once()

@responses.activate
@patch.dict("os.environ", {"RSTUDIO_PRODUCT": "CONNECT"})
@patch.dict("os.environ", {"POSIT_PRODUCT": "CONNECT"})
def test_with_user_session_token(self):
api_key = "12345"
url = "https://connect.example.com"
Expand Down Expand Up @@ -117,7 +117,7 @@ def test_with_user_session_token(self):
assert visitor_client.cfg.api_key == "api-key"

@responses.activate
@patch.dict("os.environ", {"RSTUDIO_PRODUCT": "CONNECT"})
@patch.dict("os.environ", {"POSIT_PRODUCT": "CONNECT"})
def test_with_user_session_token_bad_exchange_response_body(self):
api_key = "12345"
url = "https://connect.example.com"
Expand All @@ -143,7 +143,7 @@ def test_with_user_session_token_bad_exchange_response_body(self):
client.with_user_session_token("cit")
assert str(err.value) == "Unable to retrieve token."

@patch.dict("os.environ", {"RSTUDIO_PRODUCT": "CONNECT"})
@patch.dict("os.environ", {"POSIT_PRODUCT": "CONNECT"})
def test_with_user_session_token_bad_token_deployed(self):
api_key = "12345"
url = "https://connect.example.com"
Expand Down
54 changes: 54 additions & 0 deletions tests/posit/test_environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# pyright: reportFunctionMemberAccess=false
from unittest.mock import patch

import pytest

from posit.environment import (
get_product,
is_local,
is_running_on_connect,
is_running_on_workbench,
)


@pytest.mark.parametrize(
("posit_product", "rstudio_product", "expected"),
[
("CONNECT", None, "CONNECT"),
(None, "WORKBENCH", "WORKBENCH"),
("CONNECT", "WORKBENCH", "CONNECT"),
(None, None, None),
],
)
def test_get_product(posit_product, rstudio_product, expected):
env = {}
if posit_product is not None:
env["POSIT_PRODUCT"] = posit_product
if rstudio_product is not None:
env["RSTUDIO_PRODUCT"] = rstudio_product
with patch.dict("os.environ", env, clear=True):
assert get_product() == expected


def test_is_local():
with patch("posit.environment.get_product", return_value=None):
assert is_local() is True

with patch("posit.environment.get_product", return_value="CONNECT"):
assert is_local() is False


def test_is_running_on_connect():
with patch("posit.environment.get_product", return_value="CONNECT"):
assert is_running_on_connect() is True

with patch("posit.environment.get_product", return_value="WORKBENCH"):
assert is_running_on_connect() is False


def test_is_running_on_workbench():
with patch("posit.environment.get_product", return_value="WORKBENCH"):
assert is_running_on_workbench() is True

with patch("posit.environment.get_product", return_value="CONNECT"):
assert is_running_on_workbench() is False