Skip to content

Commit a4ef985

Browse files
authored
feat: Prepend SageMaker Studio App Type to boto3 User Agent string (#4440)
1 parent 5a22f4d commit a4ef985

File tree

3 files changed

+212
-61
lines changed

3 files changed

+212
-61
lines changed

src/sagemaker/user_agent.py

+59-11
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,18 @@
1515

1616
import platform
1717
import sys
18+
import json
19+
import os
1820

1921
import importlib_metadata
2022

23+
SDK_PREFIX = "AWS-SageMaker-Python-SDK"
24+
STUDIO_PREFIX = "AWS-SageMaker-Studio"
25+
NOTEBOOK_PREFIX = "AWS-SageMaker-Notebook-Instance"
26+
27+
NOTEBOOK_METADATA_FILE = "/etc/opt/ml/sagemaker-notebook-instance-version.txt"
28+
STUDIO_METADATA_FILE = "/opt/ml/metadata/resource-metadata.json"
29+
2130
SDK_VERSION = importlib_metadata.version("sagemaker")
2231
OS_NAME = platform.system() or "UnresolvedOS"
2332
OS_VERSION = platform.release() or "UnresolvedOSVersion"
@@ -27,30 +36,69 @@
2736
)
2837

2938

39+
def process_notebook_metadata_file():
40+
"""Check if the platform is SageMaker Notebook, if yes, return the InstanceType
41+
42+
Returns:
43+
str: The InstanceType of the SageMaker Notebook if it exists, otherwise None
44+
"""
45+
if os.path.exists(NOTEBOOK_METADATA_FILE):
46+
with open(NOTEBOOK_METADATA_FILE, "r") as sagemaker_nbi_file:
47+
return sagemaker_nbi_file.read().strip()
48+
49+
return None
50+
51+
52+
def process_studio_metadata_file():
53+
"""Check if the platform is SageMaker Studio, if yes, return the AppType
54+
55+
Returns:
56+
str: The AppType of the SageMaker Studio if it exists, otherwise None
57+
"""
58+
if os.path.exists(STUDIO_METADATA_FILE):
59+
with open(STUDIO_METADATA_FILE, "r") as sagemaker_studio_file:
60+
metadata = json.load(sagemaker_studio_file)
61+
return metadata.get("AppType")
62+
63+
return None
64+
65+
3066
def determine_prefix(user_agent=""):
31-
"""Placeholder docstring"""
32-
prefix = "AWS-SageMaker-Python-SDK/{}".format(SDK_VERSION)
67+
"""Determines the prefix for the user agent string.
68+
69+
Args:
70+
user_agent (str): The user agent string to prepend the prefix to.
71+
72+
Returns:
73+
str: The user agent string with the prefix prepended.
74+
"""
75+
prefix = "{}/{}".format(SDK_PREFIX, SDK_VERSION)
3376

3477
if PYTHON_VERSION not in user_agent:
3578
prefix = "{} {}".format(prefix, PYTHON_VERSION)
3679

3780
if OS_NAME_VERSION not in user_agent:
3881
prefix = "{} {}".format(prefix, OS_NAME_VERSION)
3982

40-
try:
41-
with open("/etc/opt/ml/sagemaker-notebook-instance-version.txt") as sagemaker_nbi_file:
42-
prefix = "{} AWS-SageMaker-Notebook-Instance/{}".format(
43-
prefix, sagemaker_nbi_file.read().strip()
44-
)
45-
except IOError:
46-
# This file isn't expected to always exist, and we DO want to silently ignore failures.
47-
pass
83+
# Get the notebook instance type and prepend it to the user agent string if exists
84+
notebook_instance_type = process_notebook_metadata_file()
85+
if notebook_instance_type:
86+
prefix = "{} {}/{}".format(prefix, NOTEBOOK_PREFIX, notebook_instance_type)
87+
88+
# Get the studio app type and prepend it to the user agent string if exists
89+
studio_app_type = process_studio_metadata_file()
90+
if studio_app_type:
91+
prefix = "{} {}/{}".format(prefix, STUDIO_PREFIX, studio_app_type)
4892

4993
return prefix
5094

5195

5296
def prepend_user_agent(client):
53-
"""Placeholder docstring"""
97+
"""Prepends the user agent string with the SageMaker Python SDK version.
98+
99+
Args:
100+
client (botocore.client.BaseClient): The client to prepend the user agent string for.
101+
"""
54102
prefix = determine_prefix(client._client_config.user_agent)
55103

56104
if client._client_config.user_agent is None:

tests/unit/test_session.py

+49-50
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
from sagemaker.inputs import BatchDataCaptureConfig
4242
from sagemaker.config import MODEL_CONTAINERS_PATH
4343
from sagemaker.utils import update_list_of_dicts_with_values_from_config
44+
from sagemaker.user_agent import (
45+
SDK_PREFIX,
46+
STUDIO_PREFIX,
47+
NOTEBOOK_PREFIX,
48+
)
4449
from sagemaker.compute_resource_requirements.resource_requirements import ResourceRequirements
4550
from tests.unit import (
4651
SAGEMAKER_CONFIG_MONITORING_SCHEDULE,
@@ -904,70 +909,64 @@ def test_delete_model(boto_session):
904909

905910

906911
def test_user_agent_injected(boto_session):
907-
assert (
908-
"AWS-SageMaker-Python-SDK" not in boto_session.client("sagemaker")._client_config.user_agent
909-
)
912+
assert SDK_PREFIX not in boto_session.client("sagemaker")._client_config.user_agent
910913

911914
sess = Session(boto_session)
912915

913-
assert "AWS-SageMaker-Python-SDK" in sess.sagemaker_client._client_config.user_agent
914-
assert "AWS-SageMaker-Python-SDK" in sess.sagemaker_runtime_client._client_config.user_agent
915-
assert "AWS-SageMaker-Python-SDK" in sess.sagemaker_metrics_client._client_config.user_agent
916-
assert "AWS-SageMaker-Notebook-Instance" not in sess.sagemaker_client._client_config.user_agent
917-
assert (
918-
"AWS-SageMaker-Notebook-Instance"
919-
not in sess.sagemaker_runtime_client._client_config.user_agent
920-
)
921-
assert (
922-
"AWS-SageMaker-Notebook-Instance"
923-
not in sess.sagemaker_metrics_client._client_config.user_agent
924-
)
916+
for client in [
917+
sess.sagemaker_client,
918+
sess.sagemaker_runtime_client,
919+
sess.sagemaker_metrics_client,
920+
]:
921+
assert SDK_PREFIX in client._client_config.user_agent
922+
assert NOTEBOOK_PREFIX not in client._client_config.user_agent
923+
assert STUDIO_PREFIX not in client._client_config.user_agent
925924

926925

927-
def test_user_agent_injected_with_nbi(boto_session):
928-
assert (
929-
"AWS-SageMaker-Python-SDK" not in boto_session.client("sagemaker")._client_config.user_agent
926+
@patch("sagemaker.user_agent.process_notebook_metadata_file", return_value="ml.t3.medium")
927+
def test_user_agent_injected_with_nbi(
928+
mock_process_notebook_metadata_file,
929+
boto_session,
930+
):
931+
assert SDK_PREFIX not in boto_session.client("sagemaker")._client_config.user_agent
932+
933+
sess = Session(
934+
boto_session=boto_session,
930935
)
931936

932-
with patch("six.moves.builtins.open", mock_open(read_data="120.0-0")) as mo:
933-
sess = Session(boto_session)
937+
for client in [
938+
sess.sagemaker_client,
939+
sess.sagemaker_runtime_client,
940+
sess.sagemaker_metrics_client,
941+
]:
942+
mock_process_notebook_metadata_file.assert_called()
934943

935-
mo.assert_called_with("/etc/opt/ml/sagemaker-notebook-instance-version.txt")
944+
assert SDK_PREFIX in client._client_config.user_agent
945+
assert NOTEBOOK_PREFIX in client._client_config.user_agent
946+
assert STUDIO_PREFIX not in client._client_config.user_agent
936947

937-
assert "AWS-SageMaker-Python-SDK" in sess.sagemaker_client._client_config.user_agent
938-
assert "AWS-SageMaker-Python-SDK" in sess.sagemaker_runtime_client._client_config.user_agent
939-
assert "AWS-SageMaker-Python-SDK" in sess.sagemaker_metrics_client._client_config.user_agent
940-
assert "AWS-SageMaker-Notebook-Instance" in sess.sagemaker_client._client_config.user_agent
941-
assert (
942-
"AWS-SageMaker-Notebook-Instance" in sess.sagemaker_runtime_client._client_config.user_agent
943-
)
944-
assert (
945-
"AWS-SageMaker-Notebook-Instance" in sess.sagemaker_metrics_client._client_config.user_agent
946-
)
947948

949+
@patch("sagemaker.user_agent.process_studio_metadata_file", return_value="dymmy-app-type")
950+
def test_user_agent_injected_with_studio_app_type(
951+
mock_process_studio_metadata_file,
952+
boto_session,
953+
):
954+
assert SDK_PREFIX not in boto_session.client("sagemaker")._client_config.user_agent
948955

949-
def test_user_agent_injected_with_nbi_ioerror(boto_session):
950-
assert (
951-
"AWS-SageMaker-Python-SDK" not in boto_session.client("sagemaker")._client_config.user_agent
956+
sess = Session(
957+
boto_session=boto_session,
952958
)
953959

954-
with patch("six.moves.builtins.open", MagicMock(side_effect=IOError("File not found"))) as mo:
955-
sess = Session(boto_session)
960+
for client in [
961+
sess.sagemaker_client,
962+
sess.sagemaker_runtime_client,
963+
sess.sagemaker_metrics_client,
964+
]:
965+
mock_process_studio_metadata_file.assert_called()
956966

957-
mo.assert_called_with("/etc/opt/ml/sagemaker-notebook-instance-version.txt")
958-
959-
assert "AWS-SageMaker-Python-SDK" in sess.sagemaker_client._client_config.user_agent
960-
assert "AWS-SageMaker-Python-SDK" in sess.sagemaker_runtime_client._client_config.user_agent
961-
assert "AWS-SageMaker-Python-SDK" in sess.sagemaker_metrics_client._client_config.user_agent
962-
assert "AWS-SageMaker-Notebook-Instance" not in sess.sagemaker_client._client_config.user_agent
963-
assert (
964-
"AWS-SageMaker-Notebook-Instance"
965-
not in sess.sagemaker_runtime_client._client_config.user_agent
966-
)
967-
assert (
968-
"AWS-SageMaker-Notebook-Instance"
969-
not in sess.sagemaker_metrics_client._client_config.user_agent
970-
)
967+
assert SDK_PREFIX in client._client_config.user_agent
968+
assert NOTEBOOK_PREFIX not in client._client_config.user_agent
969+
assert STUDIO_PREFIX in client._client_config.user_agent
971970

972971

973972
def test_training_input_all_defaults():

tests/unit/test_user_agent.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
from __future__ import absolute_import
14+
15+
import json
16+
from mock import MagicMock, patch, mock_open
17+
18+
19+
from sagemaker.user_agent import (
20+
SDK_PREFIX,
21+
SDK_VERSION,
22+
PYTHON_VERSION,
23+
OS_NAME_VERSION,
24+
NOTEBOOK_PREFIX,
25+
STUDIO_PREFIX,
26+
process_notebook_metadata_file,
27+
process_studio_metadata_file,
28+
determine_prefix,
29+
prepend_user_agent,
30+
)
31+
32+
33+
# Test process_notebook_metadata_file function
34+
def test_process_notebook_metadata_file_exists(tmp_path):
35+
notebook_file = tmp_path / "sagemaker-notebook-instance-version.txt"
36+
notebook_file.write_text("instance_type")
37+
38+
with patch("os.path.exists", return_value=True):
39+
with patch("builtins.open", mock_open(read_data=notebook_file.read_text())):
40+
assert process_notebook_metadata_file() == "instance_type"
41+
42+
43+
def test_process_notebook_metadata_file_not_exists(tmp_path):
44+
with patch("os.path.exists", return_value=False):
45+
assert process_notebook_metadata_file() is None
46+
47+
48+
# Test process_studio_metadata_file function
49+
def test_process_studio_metadata_file_exists(tmp_path):
50+
studio_file = tmp_path / "resource-metadata.json"
51+
studio_file.write_text(json.dumps({"AppType": "studio_type"}))
52+
53+
with patch("os.path.exists", return_value=True):
54+
with patch("builtins.open", mock_open(read_data=studio_file.read_text())):
55+
assert process_studio_metadata_file() == "studio_type"
56+
57+
58+
def test_process_studio_metadata_file_not_exists(tmp_path):
59+
with patch("os.path.exists", return_value=False):
60+
assert process_studio_metadata_file() is None
61+
62+
63+
# Test determine_prefix function
64+
def test_determine_prefix_notebook_instance_type(monkeypatch):
65+
monkeypatch.setattr(
66+
"sagemaker.user_agent.process_notebook_metadata_file", lambda: "instance_type"
67+
)
68+
assert (
69+
determine_prefix()
70+
== f"{SDK_PREFIX}/{SDK_VERSION} {PYTHON_VERSION} {OS_NAME_VERSION} {NOTEBOOK_PREFIX}/instance_type"
71+
)
72+
73+
74+
def test_determine_prefix_studio_app_type(monkeypatch):
75+
monkeypatch.setattr(
76+
"sagemaker.user_agent.process_studio_metadata_file", lambda: "studio_app_type"
77+
)
78+
assert (
79+
determine_prefix()
80+
== f"{SDK_PREFIX}/{SDK_VERSION} {PYTHON_VERSION} {OS_NAME_VERSION} {STUDIO_PREFIX}/studio_app_type"
81+
)
82+
83+
84+
def test_determine_prefix_no_metadata(monkeypatch):
85+
monkeypatch.setattr("sagemaker.user_agent.process_notebook_metadata_file", lambda: None)
86+
monkeypatch.setattr("sagemaker.user_agent.process_studio_metadata_file", lambda: None)
87+
assert determine_prefix() == f"{SDK_PREFIX}/{SDK_VERSION} {PYTHON_VERSION} {OS_NAME_VERSION}"
88+
89+
90+
# Test prepend_user_agent function
91+
def test_prepend_user_agent_existing_user_agent(monkeypatch):
92+
client = MagicMock()
93+
client._client_config.user_agent = "existing_user_agent"
94+
monkeypatch.setattr("sagemaker.user_agent.determine_prefix", lambda _: "prefix")
95+
prepend_user_agent(client)
96+
assert client._client_config.user_agent == "prefix existing_user_agent"
97+
98+
99+
def test_prepend_user_agent_no_user_agent(monkeypatch):
100+
client = MagicMock()
101+
client._client_config.user_agent = None
102+
monkeypatch.setattr("sagemaker.user_agent.determine_prefix", lambda _: "prefix")
103+
prepend_user_agent(client)
104+
assert client._client_config.user_agent == "prefix"

0 commit comments

Comments
 (0)