diff --git a/sagemaker-core/src/sagemaker/algorithm.py b/sagemaker-core/src/sagemaker/algorithm.py new file mode 100644 index 0000000000..a75a8dfa8f --- /dev/null +++ b/sagemaker-core/src/sagemaker/algorithm.py @@ -0,0 +1,26 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.algorithm`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.algorithm", + replacement="`ModelTrainer` in the sagemaker-train package", + v3_import="from sagemaker.train import ModelTrainer", +) diff --git a/sagemaker-core/src/sagemaker/automl.py b/sagemaker-core/src/sagemaker/automl.py new file mode 100644 index 0000000000..8c2a91b7a3 --- /dev/null +++ b/sagemaker-core/src/sagemaker/automl.py @@ -0,0 +1,25 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.automl`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.automl", + replacement="the AutoML resources under sagemaker.core", +) diff --git a/sagemaker-core/src/sagemaker/base_predictor.py b/sagemaker-core/src/sagemaker/base_predictor.py new file mode 100644 index 0000000000..20d2455175 --- /dev/null +++ b/sagemaker-core/src/sagemaker/base_predictor.py @@ -0,0 +1,25 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.base_predictor`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.base_predictor", + replacement="the Endpoint resource in the sagemaker-core package", +) diff --git a/sagemaker-core/src/sagemaker/core/__init__.py b/sagemaker-core/src/sagemaker/core/__init__.py index 498c85390a..ad6225aa63 100644 --- a/sagemaker-core/src/sagemaker/core/__init__.py +++ b/sagemaker-core/src/sagemaker/core/__init__.py @@ -1,9 +1,17 @@ """SageMaker Core package for low-level resource management and SDK foundations.""" + from sagemaker.core.utils.utils import enable_textual_rich_console_and_traceback +from sagemaker.core.deprecations import register_removed_module_finder enable_textual_rich_console_and_traceback() +# Install the fallback finder that gives actionable migration guidance for v2 +# modules removed in v3 but not individually tombstoned. sagemaker-core is the +# universal dependency of every v3 package, so registering here guarantees the +# finder is active whenever the SDK is used. +register_removed_module_finder() + # Job management from sagemaker.core.job import _Job # noqa: F401 from sagemaker.core.processing import ( # noqa: F401 diff --git a/sagemaker-core/src/sagemaker/core/deprecations.py b/sagemaker-core/src/sagemaker/core/deprecations.py index a8ba298082..cd9cccdf6d 100644 --- a/sagemaker-core/src/sagemaker/core/deprecations.py +++ b/sagemaker-core/src/sagemaker/core/deprecations.py @@ -13,13 +13,127 @@ """Module for deprecation abstractions.""" from __future__ import absolute_import +import importlib.abc import logging +import sys import warnings logger = logging.getLogger(__name__) V2_URL = "https://sagemaker.readthedocs.io/en/stable/v2.html" +# Migration guide for users moving from the v2 SDK to v3. +V3_MIGRATION_URL = "https://github.com/aws/sagemaker-python-sdk/blob/master/migration.md" + +# Real top-level ``sagemaker.*`` names that ship in v3. The fallback finder must +# never intercept these -- even when a package (e.g. sagemaker-train) is simply +# not installed, its absence should surface the normal error, not a bogus +# "removed in v3" message. +_KNOWN_V3_TOPLEVEL = frozenset({"core", "train", "serve", "mlops", "lineage", "ai_registry"}) + + +def _emit_removed_telemetry(module): + """Best-effort telemetry that a removed v2 module was imported. + + Imported lazily and fully guarded so that telemetry can never delay or break + the import/error path. See + ``sagemaker.core.telemetry.telemetry_logging.emit_removed_interface_telemetry`` + for the time-bounded, opt-out-able implementation. + """ + try: + from sagemaker.core.telemetry.telemetry_logging import ( + emit_removed_interface_telemetry, + ) + + emit_removed_interface_telemetry(module) + except Exception: # pylint: disable=W0703 + logger.debug("Removed-interface telemetry hook failed; continuing.") + + +def raise_removed_in_v3(module, replacement=None, v3_import=None): + """Warn and then raise an actionable error for a v2 module removed in v3. + + The v2 SDK exposed top-level modules (e.g. ``sagemaker.estimator``) that no + longer exist in v3. Importing one would otherwise fail with a bare + ``ModuleNotFoundError: No module named 'sagemaker.estimator'`` that gives the + caller no path forward. This helper is called from lightweight "tombstone" + modules that stand in for those removed names: it emits a + ``DeprecationWarning`` and then raises a ``ModuleNotFoundError`` whose message + names the v3 replacement and links the migration guide. + + Args: + module (str): The removed v2 module path, e.g. ``"sagemaker.estimator"``. + replacement (str): Human readable v3 replacement, e.g. + ``"ModelTrainer in the sagemaker-train package"``. Optional. + v3_import (str): The exact v3 import statement, e.g. + ``"from sagemaker.train import ModelTrainer"``. Quoted verbatim so the + caller can copy-paste it. Optional. + + Raises: + ModuleNotFoundError: always, after emitting the deprecation warning. + """ + msg = f"`{module}` was removed in the SageMaker Python SDK v3." + if replacement: + msg += f" Use {replacement}." + if v3_import: + msg += f" ({v3_import})" + msg += f"\nSee {V3_MIGRATION_URL} for the migration guide." + + _emit_removed_telemetry(module) + warnings.warn(msg, DeprecationWarning, stacklevel=2) + logger.warning(msg) + raise ModuleNotFoundError(msg, name=module) + + +class _RemovedV2ModuleFinder(importlib.abc.MetaPathFinder): + """Fallback finder that gives actionable guidance for removed v2 modules. + + Curated removals ship as explicit "tombstone" modules (e.g. + ``sagemaker/estimator.py``) that raise a precise, per-module message. This + finder is the *catch-all* for every other ``sagemaker.`` that existed + in v2 but was not individually tombstoned: instead of a bare + ``ModuleNotFoundError: No module named 'sagemaker.foo'``, the caller gets a + message that says the module is not available in v3 and points to the + migration guide. + + It is registered by appending to ``sys.meta_path``, so it only runs *after* + the normal import machinery has failed to locate the module. That ordering + guarantees it never shadows: + - real v3 packages (``sagemaker.core``, ``sagemaker.train``, ...), and + - the curated tombstone modules, which resolve as ordinary files first. + """ + + def find_spec(self, fullname, path=None, target=None): + """Intercept only genuinely-missing top-level ``sagemaker.`` imports.""" + if not fullname.startswith("sagemaker."): + return None + leaf = fullname[len("sagemaker.") :] + # Only guard top-level names; never touch real v3 subpackages. + if "." in leaf or leaf in _KNOWN_V3_TOPLEVEL: + return None + + msg = ( + f"`{fullname}` is not available in the SageMaker Python SDK v3. " + "It may have been removed or moved to a new location." + f"\nSee {V3_MIGRATION_URL} for the migration guide." + ) + _emit_removed_telemetry(fullname) + warnings.warn(msg, DeprecationWarning, stacklevel=2) + logger.warning(msg) + raise ModuleNotFoundError(msg, name=fullname) + + +def register_removed_module_finder(): + """Install the fallback finder for removed v2 modules (idempotent). + + Appends a single ``_RemovedV2ModuleFinder`` to ``sys.meta_path`` so it acts + as a last resort. Safe to call multiple times -- it installs at most one + instance per process. + """ + if any(isinstance(f, _RemovedV2ModuleFinder) for f in sys.meta_path): + return + sys.meta_path.append(_RemovedV2ModuleFinder()) + def _warn(msg, sdk_version=None): """Generic warning raiser referencing V2 diff --git a/sagemaker-core/src/sagemaker/core/telemetry/constants.py b/sagemaker-core/src/sagemaker/core/telemetry/constants.py index 04231ef8a0..360e49d2a7 100644 --- a/sagemaker-core/src/sagemaker/core/telemetry/constants.py +++ b/sagemaker-core/src/sagemaker/core/telemetry/constants.py @@ -32,6 +32,7 @@ class Feature(Enum): PROCESSING = 18 MODEL_CUSTOMIZATION_NOVA = 19 MODEL_CUSTOMIZATION_OSS = 20 + DEPRECATED_V2_INTERFACE = 21 def __str__(self): # pylint: disable=E0307 """Return the feature name.""" diff --git a/sagemaker-core/src/sagemaker/core/telemetry/telemetry_logging.py b/sagemaker-core/src/sagemaker/core/telemetry/telemetry_logging.py index 1fb0f80f00..87ffab92b8 100644 --- a/sagemaker-core/src/sagemaker/core/telemetry/telemetry_logging.py +++ b/sagemaker-core/src/sagemaker/core/telemetry/telemetry_logging.py @@ -16,6 +16,7 @@ import os import platform import sys +import threading from time import perf_counter from typing import List import functools @@ -23,6 +24,7 @@ from urllib.parse import quote import boto3 +from botocore.config import Config from sagemaker.core.helper.session_helper import Session from sagemaker.core.telemetry.attribution import _CREATED_BY_ENV_VAR from sagemaker.core.telemetry.resource_creation import get_resource_arn @@ -64,6 +66,7 @@ str(Feature.PROCESSING): 18, str(Feature.MODEL_CUSTOMIZATION_NOVA): 19, str(Feature.MODEL_CUSTOMIZATION_OSS): 20, + str(Feature.DEPRECATED_V2_INTERFACE): 21, } STATUS_TO_CODE = { @@ -139,9 +142,7 @@ def wrapper(*args, **kwargs): FEATURE_TO_CODE[str(Feature.MODEL_CUSTOMIZATION_OSS)] ) except Exception: # pylint: disable=W0703 - logger.debug( - "Unable to determine NOVA/OSS model type for telemetry." - ) + logger.debug("Unable to determine NOVA/OSS model type for telemetry.") if ( hasattr(sagemaker_session, "sagemaker_config") @@ -329,3 +330,145 @@ def _get_default_sagemaker_session(): sagemaker_session = Session(boto_session=boto_session) return sagemaker_session + + +# --- Session-less telemetry for removed v2 interfaces (import-time) --- +# +# This path resolves an AWS account id and emits a telemetry ping when a caller +# imports a v2 interface that was removed in v3. It sends the same account-id +# field to the same endpoint as the session-based emitter above (no new data +# category), and honors the same TelemetryOptOut config. Because it runs at +# IMPORT time (no sagemaker_session exists), it must be strictly best-effort and +# must never block the import or change the error raised. + +# Additional env-var opt-out for the removed-interface telemetry. This is a +# convenience fallback on top of the standard TelemetryOptOut SDK-defaults config +# (which is honored session-lessly; see _deprecation_telemetry_opted_out). +DEPRECATION_TELEMETRY_OPT_OUT_ENV_VAR = "SAGEMAKER_DEPRECATION_TELEMETRY_OPT_OUT" + +# Hard ceiling (seconds) on the entire emit so a slow credential/STS/S3 path can +# never delay the ModuleNotFoundError the caller is about to receive. +_DEPRECATION_TELEMETRY_BUDGET_SECONDS = 1.5 + +# Emit at most once per removed module per process. +_deprecation_telemetry_sent = set() + + +def _deprecation_telemetry_opted_out(): + """Return True if removed-interface telemetry is disabled. + + Honors the same ``TelemetryOptOut`` SDK-defaults config the session-based + emitter uses, so a user who opted out of telemetry is opted out here too. + Because there is no ``sagemaker_session`` at import time, the config is + loaded session-lessly via ``load_sagemaker_config()`` and passed to + ``resolve_value_from_config`` through its ``sagemaker_config`` parameter. + The ``SAGEMAKER_DEPRECATION_TELEMETRY_OPT_OUT`` environment variable is + honored as an additional fallback (and short-circuit for environments where + loading the config is undesirable). + """ + # Env var fallback first: cheap, and lets users opt out without any config + # file lookup on the import path. + if os.environ.get(DEPRECATION_TELEMETRY_OPT_OUT_ENV_VAR): + return True + + try: + from sagemaker.core.config.config import load_sagemaker_config + + sagemaker_config = load_sagemaker_config() + return bool( + resolve_value_from_config( + direct_input=None, + config_path=TELEMETRY_OPT_OUT_PATH, + default_value=False, + sagemaker_config=sagemaker_config, + ) + ) + except Exception: # pylint: disable=W0703 + # If the config cannot be loaded/resolved, do not block telemetry on it; + # fall back to "not opted out" (env var already checked above). + logger.debug("Removed-interface telemetry: could not resolve TelemetryOptOut config.") + return False + + +def _resolve_account_and_region_sessionless(): + """Best-effort account id + region without an existing session. + + Builds a short-timeout boto3 session and calls STS. Any failure (no creds, + offline, IMDS timeout, STS error) yields ("NotAvailable", default region). + Kept small and defensive because it runs on a failing-import path. + """ + account_id = "NotAvailable" + region = DEFAULT_AWS_REGION + try: + # Tight timeouts + no retries so a blocked IMDS/STS cannot stall us. + cfg = Config(connect_timeout=1, read_timeout=1, retries={"max_attempts": 0}) + boto_session = boto3.Session() + region = boto_session.region_name or DEFAULT_AWS_REGION + sts = boto_session.client("sts", config=cfg) + account_id = sts.get_caller_identity().get("Account", "NotAvailable") + except Exception: # pylint: disable=W0703 + # No creds / offline / timeout / any error -> stay anonymous. + logger.debug("Removed-interface telemetry: account id unavailable.") + return account_id, region + + +def _emit_removed_interface_telemetry_blocking(module): + """Resolve identity and send the telemetry request (runs in a worker).""" + account_id, region = _resolve_account_and_region_sessionless() + try: + Region(region) + except ValueError: + logger.debug("Removed-interface telemetry: unsupported region, skipping.") + return + extra = ( + f"{module}" + f"&x-sdkVersion={SDK_VERSION}" + f"&x-env={PYTHON_VERSION}" + f"&x-sys={OS_NAME_VERSION}" + ) + url = _construct_url( + accountId=account_id, + region=region, + status=str(STATUS_TO_CODE[str(Status.FAILURE)]), + feature=str(FEATURE_TO_CODE[str(Feature.DEPRECATED_V2_INTERFACE)]), + failure_reason=None, + failure_type=None, + extra_info=extra, + ) + _requests_helper(url, 2) + + +def emit_removed_interface_telemetry(module): + """Best-effort, time-bounded telemetry for importing a removed v2 module. + + Emits at most once per module per process, honors + ``SAGEMAKER_DEPRECATION_TELEMETRY_OPT_OUT``, and is hard-capped by + ``_DEPRECATION_TELEMETRY_BUDGET_SECONDS`` via a worker thread so it can never + block or delay the caller's import. Never raises. + + Args: + module (str): The removed v2 module path, e.g. ``"sagemaker.estimator"``. + """ + try: + if _deprecation_telemetry_opted_out(): + return + if module in _deprecation_telemetry_sent: + return + _deprecation_telemetry_sent.add(module) + + def _guarded_worker(): + # Never let a worker-thread exception surface (pytest and some + # runtimes report unhandled thread exceptions loudly). + try: + _emit_removed_interface_telemetry_blocking(module) + except Exception: # pylint: disable=W0703 + logger.debug("Removed-interface telemetry worker failed.") + + worker = threading.Thread(target=_guarded_worker, daemon=True) + worker.start() + # Bound the wait; if the network/STS path is slow we return anyway and + # the daemon thread is abandoned (it dies with the process). + worker.join(timeout=_DEPRECATION_TELEMETRY_BUDGET_SECONDS) + except Exception: # pylint: disable=W0703 + # Telemetry must never interfere with the import/error path. + logger.debug("Removed-interface telemetry not emitted.") diff --git a/sagemaker-core/src/sagemaker/estimator.py b/sagemaker-core/src/sagemaker/estimator.py new file mode 100644 index 0000000000..f0f2c16d66 --- /dev/null +++ b/sagemaker-core/src/sagemaker/estimator.py @@ -0,0 +1,26 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.estimator`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.estimator", + replacement="`ModelTrainer` in the sagemaker-train package", + v3_import="from sagemaker.train import ModelTrainer", +) diff --git a/sagemaker-core/src/sagemaker/model.py b/sagemaker-core/src/sagemaker/model.py new file mode 100644 index 0000000000..1090687bcf --- /dev/null +++ b/sagemaker-core/src/sagemaker/model.py @@ -0,0 +1,26 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.model`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.model", + replacement="`ModelBuilder` in the sagemaker-serve package", + v3_import="from sagemaker.serve import ModelBuilder", +) diff --git a/sagemaker-core/src/sagemaker/multidatamodel.py b/sagemaker-core/src/sagemaker/multidatamodel.py new file mode 100644 index 0000000000..6f777c5dde --- /dev/null +++ b/sagemaker-core/src/sagemaker/multidatamodel.py @@ -0,0 +1,26 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.multidatamodel`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.multidatamodel", + replacement="`ModelBuilder` in the sagemaker-serve package", + v3_import="from sagemaker.serve import ModelBuilder", +) diff --git a/sagemaker-core/src/sagemaker/pipeline.py b/sagemaker-core/src/sagemaker/pipeline.py new file mode 100644 index 0000000000..04892162f7 --- /dev/null +++ b/sagemaker-core/src/sagemaker/pipeline.py @@ -0,0 +1,26 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.pipeline`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.pipeline", + replacement="`Pipeline` in the sagemaker-mlops package", + v3_import="from sagemaker.mlops.pipeline import Pipeline", +) diff --git a/sagemaker-core/src/sagemaker/predictor.py b/sagemaker-core/src/sagemaker/predictor.py new file mode 100644 index 0000000000..547be2b91a --- /dev/null +++ b/sagemaker-core/src/sagemaker/predictor.py @@ -0,0 +1,25 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.predictor`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.predictor", + replacement="the Endpoint resource in the sagemaker-core package", +) diff --git a/sagemaker-core/src/sagemaker/predictor_async.py b/sagemaker-core/src/sagemaker/predictor_async.py new file mode 100644 index 0000000000..1b3b41cebc --- /dev/null +++ b/sagemaker-core/src/sagemaker/predictor_async.py @@ -0,0 +1,26 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.predictor_async`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.predictor_async", + replacement="`ModelBuilder.deploy` in the sagemaker-serve package", + v3_import="from sagemaker.serve import ModelBuilder", +) diff --git a/sagemaker-core/src/sagemaker/processing.py b/sagemaker-core/src/sagemaker/processing.py new file mode 100644 index 0000000000..c9900985aa --- /dev/null +++ b/sagemaker-core/src/sagemaker/processing.py @@ -0,0 +1,26 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.processing`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.processing", + replacement="`DataProcessor` in the sagemaker-mlops package", + v3_import="from sagemaker.mlops.processing import DataProcessor", +) diff --git a/sagemaker-core/src/sagemaker/transformer.py b/sagemaker-core/src/sagemaker/transformer.py new file mode 100644 index 0000000000..0fdec3e888 --- /dev/null +++ b/sagemaker-core/src/sagemaker/transformer.py @@ -0,0 +1,25 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.transformer`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.transformer", + replacement="the TransformJob resource in the sagemaker-core package", +) diff --git a/sagemaker-core/src/sagemaker/tuner.py b/sagemaker-core/src/sagemaker/tuner.py new file mode 100644 index 0000000000..50c42185ec --- /dev/null +++ b/sagemaker-core/src/sagemaker/tuner.py @@ -0,0 +1,25 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.tuner`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.tuner", + replacement="the hyperparameter tuning resources in the sagemaker-core package", +) diff --git a/sagemaker-core/tests/unit/test_removed_v2_modules.py b/sagemaker-core/tests/unit/test_removed_v2_modules.py new file mode 100644 index 0000000000..4f1d6aa620 --- /dev/null +++ b/sagemaker-core/tests/unit/test_removed_v2_modules.py @@ -0,0 +1,306 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Tests that removed v2 modules raise actionable guidance in v3.""" +from __future__ import absolute_import + +import importlib +import warnings + +import pytest + +from sagemaker.core.deprecations import ( + raise_removed_in_v3, + register_removed_module_finder, + _RemovedV2ModuleFinder, + V3_MIGRATION_URL, +) + +# Removed v2 module -> a substring we expect in the guidance message. +REMOVED_MODULES = { + "sagemaker.estimator": "ModelTrainer", + "sagemaker.model": "ModelBuilder", + "sagemaker.predictor": "Endpoint", + "sagemaker.base_predictor": "Endpoint", + "sagemaker.predictor_async": "ModelBuilder", + "sagemaker.transformer": "TransformJob", + "sagemaker.tuner": "hyperparameter tuning", + "sagemaker.processing": "DataProcessor", + "sagemaker.pipeline": "Pipeline", + "sagemaker.multidatamodel": "ModelBuilder", + "sagemaker.automl": "AutoML", + "sagemaker.algorithm": "ModelTrainer", +} + + +def _fresh_import(module): + """Import a module fresh, bypassing any cached (failed) import state.""" + import sys + + sys.modules.pop(module, None) + return importlib.import_module(module) + + +@pytest.mark.parametrize("module,expected", sorted(REMOVED_MODULES.items())) +def test_removed_module_raises_actionable_error(module, expected): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + with pytest.raises(ModuleNotFoundError) as exc: + _fresh_import(module) + message = str(exc.value) + assert module in message + assert "removed in the SageMaker Python SDK v3" in message + assert expected in message + assert V3_MIGRATION_URL in message + + +@pytest.mark.parametrize("module", sorted(REMOVED_MODULES)) +def test_removed_module_emits_deprecation_warning(module): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with pytest.raises(ModuleNotFoundError): + _fresh_import(module) + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + +def test_helper_includes_import_when_provided(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + with pytest.raises(ModuleNotFoundError) as exc: + raise_removed_in_v3( + module="sagemaker.estimator", + replacement="`ModelTrainer` in the sagemaker-train package", + v3_import="from sagemaker.train import ModelTrainer", + ) + message = str(exc.value) + assert "from sagemaker.train import ModelTrainer" in message + # ModuleNotFoundError carries the module name for tooling. + assert exc.value.name == "sagemaker.estimator" + + +def test_helper_without_replacement_still_actionable(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + with pytest.raises(ModuleNotFoundError) as exc: + raise_removed_in_v3(module="sagemaker.something") + message = str(exc.value) + assert "sagemaker.something" in message + assert V3_MIGRATION_URL in message + + +# --- Fallback finder (catch-all for non-tombstoned removed modules) --- + + +def test_finder_is_registered_on_meta_path(): + import sys + import sagemaker.core # noqa: F401 -- import registers the finder + + assert any(isinstance(f, _RemovedV2ModuleFinder) for f in sys.meta_path) + + +def test_register_is_idempotent(): + import sys + + register_removed_module_finder() + register_removed_module_finder() + finders = [f for f in sys.meta_path if isinstance(f, _RemovedV2ModuleFinder)] + assert len(finders) == 1 + + +def test_finder_is_appended_not_prepended(): + # As a last resort it must sit at the end so real/tombstone modules resolve + # first. Being in the second half of meta_path is a sufficient proxy. + import sys + + register_removed_module_finder() + idx = next(i for i, f in enumerate(sys.meta_path) if isinstance(f, _RemovedV2ModuleFinder)) + assert idx >= len(sys.meta_path) // 2 + + +@pytest.mark.parametrize("module", ["sagemaker.clarify", "sagemaker.session", "sagemaker.network"]) +def test_uncovered_module_falls_back_to_guidance(module): + import sagemaker.core # noqa: F401 -- ensure finder registered + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with pytest.raises(ModuleNotFoundError) as exc: + _fresh_import(module) + message = str(exc.value) + assert module in message + assert "not available in the SageMaker Python SDK v3" in message + assert V3_MIGRATION_URL in message + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + +@pytest.mark.parametrize("module", ["sagemaker.core", "sagemaker.train", "sagemaker.serve"]) +def test_finder_does_not_shadow_real_v3_packages(module): + import sagemaker.core # noqa: F401 -- ensure finder registered + + # Real v3 packages must import cleanly, untouched by the fallback finder. + _fresh_import(module) + + +def test_finder_ignores_non_sagemaker_and_nested(): + import sagemaker.core # noqa: F401 + + finder = _RemovedV2ModuleFinder() + # Not a sagemaker module -> pass through (None). + assert finder.find_spec("numpy") is None + # Nested path under a (removed) top-level -> only the top-level is guarded. + assert finder.find_spec("sagemaker.workflow.steps") is None + # Real v3 top-level -> never guarded. + assert finder.find_spec("sagemaker.train") is None + + +# --- Removed-interface telemetry (session-less, import-time) --- + + +@pytest.fixture(autouse=True) +def _reset_telemetry_state(monkeypatch): + """Isolate telemetry dedup + opt-out env var per test. + + Also neutralize the actual network/STS emit by default so the suite is + hermetic and fast; tests that assert telemetry behavior re-patch as needed. + """ + import sagemaker.core.telemetry.telemetry_logging as tl + + monkeypatch.delenv(tl.DEPRECATION_TELEMETRY_OPT_OUT_ENV_VAR, raising=False) + tl._deprecation_telemetry_sent.clear() + monkeypatch.setattr(tl, "_emit_removed_interface_telemetry_blocking", lambda module: None) + yield + tl._deprecation_telemetry_sent.clear() + + +def test_telemetry_emitted_once_per_module(monkeypatch): + import sagemaker.core.telemetry.telemetry_logging as tl + + calls = [] + # Stop before any network/STS: record invocations of the blocking worker. + monkeypatch.setattr( + tl, "_emit_removed_interface_telemetry_blocking", lambda module: calls.append(module) + ) + tl.emit_removed_interface_telemetry("sagemaker.estimator") + tl.emit_removed_interface_telemetry("sagemaker.estimator") + tl.emit_removed_interface_telemetry("sagemaker.transformer") + assert calls == ["sagemaker.estimator", "sagemaker.transformer"] + + +def test_telemetry_opt_out_env_var(monkeypatch): + import sagemaker.core.telemetry.telemetry_logging as tl + + calls = [] + monkeypatch.setattr( + tl, "_emit_removed_interface_telemetry_blocking", lambda module: calls.append(module) + ) + monkeypatch.setenv(tl.DEPRECATION_TELEMETRY_OPT_OUT_ENV_VAR, "1") + tl.emit_removed_interface_telemetry("sagemaker.estimator") + assert calls == [] + + +def test_telemetry_honors_config_opt_out(monkeypatch): + """A user who set TelemetryOptOut in SDK config is opted out here too.""" + import sagemaker.core.telemetry.telemetry_logging as tl + from sagemaker.core.config import config as core_config + + calls = [] + monkeypatch.setattr( + tl, "_emit_removed_interface_telemetry_blocking", lambda module: calls.append(module) + ) + # No env var set; opt-out comes purely from config. + monkeypatch.delenv(tl.DEPRECATION_TELEMETRY_OPT_OUT_ENV_VAR, raising=False) + monkeypatch.setattr( + core_config, + "load_sagemaker_config", + lambda *a, **k: { + "SchemaVersion": "1.0", + "SageMaker": {"PythonSDK": {"Modules": {"TelemetryOptOut": True}}}, + }, + ) + tl.emit_removed_interface_telemetry("sagemaker.estimator") + assert calls == [] + + +def test_telemetry_config_load_failure_does_not_block(monkeypatch): + """If config loading fails, telemetry still proceeds (not opted out).""" + import sagemaker.core.telemetry.telemetry_logging as tl + from sagemaker.core.config import config as core_config + + calls = [] + monkeypatch.setattr( + tl, "_emit_removed_interface_telemetry_blocking", lambda module: calls.append(module) + ) + monkeypatch.delenv(tl.DEPRECATION_TELEMETRY_OPT_OUT_ENV_VAR, raising=False) + + def _boom(*a, **k): + raise RuntimeError("bad config") + + monkeypatch.setattr(core_config, "load_sagemaker_config", _boom) + tl.emit_removed_interface_telemetry("sagemaker.estimator") + assert calls == ["sagemaker.estimator"] + + +def test_telemetry_never_raises(monkeypatch): + import sagemaker.core.telemetry.telemetry_logging as tl + + def boom(module): + raise RuntimeError("network exploded") + + monkeypatch.setattr(tl, "_emit_removed_interface_telemetry_blocking", boom) + # Must swallow the worker error; the caller must never see it. + tl.emit_removed_interface_telemetry("sagemaker.estimator") + + +def test_telemetry_is_time_bounded(monkeypatch): + import time + import sagemaker.core.telemetry.telemetry_logging as tl + + monkeypatch.setattr(tl, "_DEPRECATION_TELEMETRY_BUDGET_SECONDS", 0.2) + monkeypatch.setattr( + tl, "_emit_removed_interface_telemetry_blocking", lambda module: time.sleep(5) + ) + start = time.perf_counter() + tl.emit_removed_interface_telemetry("sagemaker.estimator") + elapsed = time.perf_counter() - start + # Bounded by the budget, not the 5s sleep. + assert elapsed < 1.0 + + +def test_account_resolution_defaults_when_unavailable(monkeypatch): + import sagemaker.core.telemetry.telemetry_logging as tl + + class _BoomSession: + region_name = None + + def client(self, *a, **k): + raise RuntimeError("no creds") + + monkeypatch.setattr(tl.boto3, "Session", lambda *a, **k: _BoomSession()) + account_id, region = tl._resolve_account_and_region_sessionless() + assert account_id == "NotAvailable" + assert region == tl.DEFAULT_AWS_REGION + + +def test_removed_import_triggers_telemetry_hook(monkeypatch): + """End-to-end: importing a removed module invokes the telemetry hook.""" + import importlib + import sys + import sagemaker.core # noqa: F401 -- ensures finder + telemetry loaded + import sagemaker.core.deprecations as dep + + seen = [] + monkeypatch.setattr(dep, "_emit_removed_telemetry", lambda module: seen.append(module)) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + sys.modules.pop("sagemaker.estimator", None) + with pytest.raises(ModuleNotFoundError): + importlib.import_module("sagemaker.estimator") + assert "sagemaker.estimator" in seen