Skip to content
27 changes: 21 additions & 6 deletions sagemaker-core/src/sagemaker/core/clarify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,12 +380,19 @@
"image/jpeg",
"image/png",
"application/x-npy",
"application/x-parquet",
),
),
SchemaOptional("accept_type"): And(
str,
Use(str.lower),
lambda s: s in ("text/csv", "application/jsonlines", "application/json"),
lambda s: s
in (
"text/csv",
"application/jsonlines",
"application/json",
"application/x-parquet",
),
),
SchemaOptional("label"): Or(str, int),
SchemaOptional("probability"): Or(str, int),
Expand Down Expand Up @@ -1021,11 +1028,13 @@ def __init__(
Must be set with ``instance_count``, ``model_name``
accept_type (str): The model output format to be used for getting inferences with the
shadow endpoint. Valid values are ``"text/csv"`` for CSV,
``"application/jsonlines"`` for JSON Lines, and ``"application/json"`` for JSON.
``"application/jsonlines"`` for JSON Lines, ``"application/json"`` for JSON, and
``"application/x-parquet"`` for Parquet.
Default is the same as ``content_type``.
content_type (str): The model input format to be used for getting inferences with the
shadow endpoint. Valid values are ``"text/csv"`` for CSV,
``"application/jsonlines"`` for JSON Lines, and ``"application/json"`` for JSON.
``"application/jsonlines"`` for JSON Lines, ``"application/json"`` for JSON, and
``"application/x-parquet"`` for Parquet.
Default is the same as ``dataset_format``.
content_template (str): A template string to be used to construct the model input from
dataset instances. It is only used, and required, when ``model_content_type`` is
Expand Down Expand Up @@ -1161,10 +1170,15 @@ def __init__(
)
self.predictor_config["endpoint_name_prefix"] = endpoint_name_prefix
if accept_type is not None:
if accept_type not in ["text/csv", "application/jsonlines", "application/json"]:
if accept_type not in [
"text/csv",
"application/jsonlines",
"application/json",
"application/x-parquet",
]:
raise ValueError(
f"Invalid accept_type {accept_type}."
f" Please choose text/csv or application/jsonlines."
f" Please choose text/csv, application/jsonlines, application/json, or application/x-parquet."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Medium: No unit test was added for the newly accepted application/x-parquet value. The existing tests in tests/unit/test_clarify.py only assert that invalid types raise, so this new acceptance path is untested. The guidance for this change notes: "Add unit tests when introducing new accepted values to ModelConfig content_type and accept_type validation to prevent regressions." (Source: #6068). Please add positive tests that construct a ModelConfig with content_type="application/x-parquet" and accept_type="application/x-parquet".

if time_series_model_config and accept_type == "text/csv":
raise ValueError(
Expand All @@ -1180,10 +1194,11 @@ def __init__(
"image/jpg",
"image/png",
"application/x-npy",
"application/x-parquet",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Medium: The validation now accepts application/x-parquet for both accept_type and content_type, but no unit test exercises this new positive path. The existing tests only cover the invalid cases (test_init_with_invalid_accept_type_raises_error, test_init_with_invalid_content_type_raises_error in tests/unit/test_clarify.py). Please add a test constructing ModelConfig(..., content_type="application/x-parquet", accept_type="application/x-parquet") and asserting it is stored in predictor_config, so the newly supported behaviour is guarded against regressions.

]:
raise ValueError(
f"Invalid content_type {content_type}."
f" Please choose text/csv or application/jsonlines."
f" Please choose text/csv, application/jsonlines, application/json, image/jpeg, image/jpg, image/png, application/x-npy, or application/x-parquet."
)
if content_type == "application/jsonlines":
if content_template is None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
number or boolean reaches the service quoted. Parameterise string-valued
fields only.
"""

from __future__ import absolute_import

import json
Expand Down Expand Up @@ -67,9 +68,7 @@ def _placeholder(obj):
if isinstance(obj, PipelineVariable):
variables.append(obj)
return "%s%d__" % (token_prefix, len(variables) - 1)
raise TypeError(
"Object of type %s is not JSON serializable" % obj.__class__.__name__
)
raise TypeError("Object of type %s is not JSON serializable" % obj.__class__.__name__)

document = json.dumps(job_config, default=_placeholder)
if not variables:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
"""Shared capture paths for producers running under a ``PipelineSession``."""

from __future__ import absolute_import

from typing import Any, Dict, List, Optional
Expand Down Expand Up @@ -87,9 +88,11 @@ def capture_training_request(pipeline_session, create_args: Dict[str, Any]) -> N
# Dict-form tags arrive in both shapes: lowercase keys from the SDK's own
# tag builders, PascalCase from callers passing wire-form dicts through.
request["Tags"] = [
Tag(key=tag.get("key", tag.get("Key")), value=tag.get("value", tag.get("Value")))
if isinstance(tag, dict)
else tag
(
Tag(key=tag.get("key", tag.get("Key")), value=tag.get("value", tag.get("Value")))
if isinstance(tag, dict)
else tag
)
for tag in request["Tags"]
]
pipeline_session._intercept_create_request(serialize(request), None, "train")
14 changes: 14 additions & 0 deletions sagemaker-core/tests/unit/test_clarify.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,20 @@ def test_init_with_endpoint_name(self):
config = ModelConfig(endpoint_name="my-endpoint")
assert config.predictor_config["endpoint_name"] == "my-endpoint"

def test_init_with_parquet_content_and_accept_type(self):
"""Test initialization with application/x-parquet content_type and accept_type."""
from sagemaker.core.clarify import ModelConfig

config = ModelConfig(
model_name="my-model",
instance_count=1,
instance_type="ml.m5.xlarge",
content_type="application/x-parquet",
accept_type="application/x-parquet",
)
assert config.predictor_config["content_type"] == "application/x-parquet"
assert config.predictor_config["accept_type"] == "application/x-parquet"

def test_init_with_invalid_endpoint_prefix_raises_error(self):
"""Test that invalid endpoint_name_prefix raises ValueError."""
from sagemaker.core.clarify import ModelConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
"""Tests for the ``JobConfigDocument`` encoder."""

from __future__ import absolute_import

import json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
"""Tests for the shared ``PipelineSession`` capture helpers."""

from __future__ import absolute_import

import json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
-- `ProcessingStep` expects `"run"` from here while interception is called with
`"process"` -- so a step guard cannot be satisfied by the interception name alone.
"""

from __future__ import absolute_import

import pytest
Expand Down
Loading