658 lines
23 KiB
Python
658 lines
23 KiB
Python
import os
|
|
import random
|
|
from typing import Any, NamedTuple
|
|
from unittest import mock
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
import sklearn.linear_model as logreg_module
|
|
from sklearn import datasets
|
|
|
|
import mlflow
|
|
from mlflow import MlflowClient
|
|
from mlflow.entities.model_registry import ModelVersion
|
|
from mlflow.environment_variables import MLFLOW_DISABLE_SCHEMA_DETAILS
|
|
from mlflow.exceptions import MlflowException
|
|
from mlflow.models import add_libraries_to_model
|
|
from mlflow.models.utils import (
|
|
_config_context,
|
|
_convert_llm_input_data,
|
|
_enforce_array,
|
|
_enforce_datatype,
|
|
_enforce_mlflow_datatype,
|
|
_enforce_object,
|
|
_enforce_property,
|
|
_flatten_nested_params,
|
|
_validate_and_get_model_code_path,
|
|
_validate_model_code_from_notebook,
|
|
get_model_version_from_model_uri,
|
|
)
|
|
from mlflow.pyfunc import _enforce_schema, _validate_prediction_input
|
|
from mlflow.types import DataType, Schema
|
|
from mlflow.types.schema import Array, ColSpec, Object, Property
|
|
|
|
|
|
class ModelWithData(NamedTuple):
|
|
model: Any
|
|
inference_data: Any
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def sklearn_knn_model():
|
|
iris = datasets.load_iris()
|
|
X = iris.data[:, :2] # we only take the first two features.
|
|
y = iris.target
|
|
logreg_model = logreg_module.LogisticRegression()
|
|
logreg_model.fit(X, y)
|
|
return ModelWithData(model=logreg_model, inference_data=X)
|
|
|
|
|
|
def random_int(lo=1, hi=1000000000):
|
|
return random.randint(int(lo), int(hi))
|
|
|
|
|
|
def test_adding_libraries_to_model_default(sklearn_knn_model):
|
|
model_name = f"wheels-test-{random_int()}"
|
|
artifact_path = "model"
|
|
model_uri = f"models:/{model_name}/1"
|
|
wheeled_model_uri = f"models:/{model_name}/2"
|
|
|
|
# Log a model
|
|
with mlflow.start_run():
|
|
run_id = mlflow.tracking.fluent._get_or_start_run().info.run_id
|
|
mlflow.sklearn.log_model(
|
|
sklearn_knn_model.model,
|
|
name=artifact_path,
|
|
registered_model_name=model_name,
|
|
)
|
|
|
|
wheeled_model_info = add_libraries_to_model(model_uri)
|
|
assert wheeled_model_info.run_id == run_id
|
|
|
|
# Verify new model version created
|
|
wheeled_model_version = get_model_version_from_model_uri(wheeled_model_uri)
|
|
assert wheeled_model_version.run_id == run_id
|
|
assert wheeled_model_version.name == model_name
|
|
|
|
|
|
def test_adding_libraries_to_model_new_run(sklearn_knn_model):
|
|
model_name = f"wheels-test-{random_int()}"
|
|
artifact_path = "model"
|
|
model_uri = f"models:/{model_name}/1"
|
|
wheeled_model_uri = f"models:/{model_name}/2"
|
|
|
|
# Log a model
|
|
with mlflow.start_run():
|
|
original_run_id = mlflow.tracking.fluent._get_or_start_run().info.run_id
|
|
mlflow.sklearn.log_model(
|
|
sklearn_knn_model.model,
|
|
name=artifact_path,
|
|
registered_model_name=model_name,
|
|
)
|
|
|
|
with mlflow.start_run():
|
|
wheeled_run_id = mlflow.tracking.fluent._get_or_start_run().info.run_id
|
|
wheeled_model_info = add_libraries_to_model(model_uri)
|
|
assert original_run_id != wheeled_run_id
|
|
assert wheeled_model_info.run_id == wheeled_run_id
|
|
|
|
# Verify new model version created
|
|
wheeled_model_version = get_model_version_from_model_uri(wheeled_model_uri)
|
|
assert wheeled_model_version.run_id == wheeled_run_id
|
|
assert wheeled_model_version.name == model_name
|
|
|
|
|
|
def test_adding_libraries_to_model_run_id_passed(sklearn_knn_model):
|
|
model_name = f"wheels-test-{random_int()}"
|
|
artifact_path = "model"
|
|
model_uri = f"models:/{model_name}/1"
|
|
wheeled_model_uri = f"models:/{model_name}/2"
|
|
|
|
# Log a model
|
|
with mlflow.start_run():
|
|
original_run_id = mlflow.tracking.fluent._get_or_start_run().info.run_id
|
|
mlflow.sklearn.log_model(
|
|
sklearn_knn_model.model,
|
|
name=artifact_path,
|
|
registered_model_name=model_name,
|
|
)
|
|
|
|
with mlflow.start_run():
|
|
wheeled_run_id = mlflow.tracking.fluent._get_or_start_run().info.run_id
|
|
|
|
wheeled_model_info = add_libraries_to_model(model_uri, run_id=wheeled_run_id)
|
|
assert original_run_id != wheeled_run_id
|
|
assert wheeled_model_info.run_id == wheeled_run_id
|
|
|
|
# Verify new model version created
|
|
wheeled_model_version = get_model_version_from_model_uri(wheeled_model_uri)
|
|
assert wheeled_model_version.run_id == wheeled_run_id
|
|
assert wheeled_model_version.name == model_name
|
|
|
|
|
|
def test_adding_libraries_to_model_new_model_name(sklearn_knn_model):
|
|
model_name = f"wheels-test-{random_int()}"
|
|
wheeled_model_name = f"wheels-test-{random_int()}"
|
|
artifact_path = "model"
|
|
model_uri = f"models:/{model_name}/1"
|
|
wheeled_model_uri = f"models:/{wheeled_model_name}/1"
|
|
|
|
# Log a model
|
|
with mlflow.start_run():
|
|
mlflow.sklearn.log_model(
|
|
sklearn_knn_model.model,
|
|
name=artifact_path,
|
|
registered_model_name=model_name,
|
|
)
|
|
|
|
with mlflow.start_run():
|
|
new_run_id = mlflow.tracking.fluent._get_or_start_run().info.run_id
|
|
wheeled_model_info = add_libraries_to_model(
|
|
model_uri, registered_model_name=wheeled_model_name
|
|
)
|
|
assert wheeled_model_info.run_id == new_run_id
|
|
|
|
# Verify new model version created
|
|
wheeled_model_version = get_model_version_from_model_uri(wheeled_model_uri)
|
|
assert wheeled_model_version.run_id == new_run_id
|
|
assert wheeled_model_version.name == wheeled_model_name
|
|
assert wheeled_model_name != model_name
|
|
|
|
|
|
def test_adding_libraries_to_model_when_version_source_None(sklearn_knn_model):
|
|
model_name = f"wheels-test-{random_int()}"
|
|
artifact_path = "model"
|
|
model_uri = f"models:/{model_name}/1"
|
|
|
|
# Log a model
|
|
with mlflow.start_run():
|
|
original_run_id = mlflow.tracking.fluent._get_or_start_run().info.run_id
|
|
mlflow.sklearn.log_model(
|
|
sklearn_knn_model.model,
|
|
name=artifact_path,
|
|
registered_model_name=model_name,
|
|
)
|
|
|
|
model_version_without_source = ModelVersion(name=model_name, version=1, creation_timestamp=124)
|
|
assert model_version_without_source.run_id is None
|
|
with mock.patch.object(
|
|
MlflowClient, "get_model_version", return_value=model_version_without_source
|
|
) as mlflow_client_mock:
|
|
wheeled_model_info = add_libraries_to_model(model_uri)
|
|
assert wheeled_model_info.run_id is not None
|
|
assert wheeled_model_info.run_id != original_run_id
|
|
mlflow_client_mock.assert_called_once_with(model_name, "1")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("data", "data_type"),
|
|
[
|
|
("string", DataType.string),
|
|
(np.int32(1), DataType.integer),
|
|
(np.int32(1), DataType.long),
|
|
(np.int32(1), DataType.double),
|
|
(True, DataType.boolean),
|
|
(1.0, DataType.double),
|
|
(np.float32(0.1), DataType.float),
|
|
(np.float32(0.1), DataType.double),
|
|
(np.int64(100), DataType.long),
|
|
(np.datetime64("2023-10-13 00:00:00"), DataType.datetime),
|
|
],
|
|
)
|
|
def test_enforce_datatype(data, data_type):
|
|
assert _enforce_datatype(data, data_type) == data
|
|
|
|
|
|
def test_enforce_datatype_with_errors():
|
|
with pytest.raises(MlflowException, match=r"Expected dtype to be DataType, got str"):
|
|
_enforce_datatype("string", "string")
|
|
|
|
with pytest.raises(
|
|
MlflowException, match=r"Failed to enforce schema of data `123` with dtype `string`"
|
|
):
|
|
_enforce_datatype(123, DataType.string)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"dtype",
|
|
[
|
|
pd.StringDtype(),
|
|
"string",
|
|
object,
|
|
None, # infers object in pandas <3.0, StringDtype in pandas 3.0
|
|
],
|
|
)
|
|
def test_enforce_mlflow_datatype_with_string_dtype(dtype):
|
|
# Test that string dtypes are handled correctly (pandas 3.0 compatibility)
|
|
series = pd.Series(["a", "b", "c"], dtype=dtype)
|
|
result = _enforce_mlflow_datatype("col", series, DataType.string)
|
|
assert result is series
|
|
|
|
|
|
def test_enforce_object():
|
|
data = {
|
|
"a": "some_sentence",
|
|
"b": b"some_bytes",
|
|
"c": ["sentence1", "sentence2"],
|
|
"d": {"str": "value", "arr": [0.1, 0.2]},
|
|
}
|
|
obj = Object([
|
|
Property("a", DataType.string),
|
|
Property("b", DataType.binary, required=False),
|
|
Property("c", Array(DataType.string)),
|
|
Property(
|
|
"d",
|
|
Object([
|
|
Property("str", DataType.string),
|
|
Property("arr", Array(DataType.double), required=False),
|
|
]),
|
|
),
|
|
])
|
|
assert _enforce_object(data, obj) == data
|
|
|
|
data = {"a": "some_sentence", "c": ["sentence1", "sentence2"], "d": {"str": "some_value"}}
|
|
assert _enforce_object(data, obj) == data
|
|
|
|
|
|
def test_enforce_object_with_errors():
|
|
with pytest.raises(MlflowException, match=r"Expected data to be dictionary, got list"):
|
|
_enforce_object(["some_sentence"], Object([Property("a", DataType.string)]))
|
|
|
|
with pytest.raises(MlflowException, match=r"Expected obj to be Object, got Property"):
|
|
_enforce_object({"a": "some_sentence"}, Property("a", DataType.string))
|
|
|
|
obj = Object([Property("a", DataType.string), Property("b", DataType.string, required=False)])
|
|
with pytest.raises(MlflowException, match=r"Missing required properties: {'a'}"):
|
|
_enforce_object({}, obj)
|
|
|
|
with pytest.raises(
|
|
MlflowException, match=r"Invalid properties not defined in the schema found: {'c'}"
|
|
):
|
|
_enforce_object({"a": "some_sentence", "c": "some_sentence"}, obj)
|
|
|
|
with pytest.raises(
|
|
MlflowException,
|
|
match=r"Failed to enforce schema for key `a`. Expected type string, received type int",
|
|
):
|
|
_enforce_object({"a": 1}, obj)
|
|
|
|
|
|
def test_enforce_property():
|
|
data = "some_sentence"
|
|
prop = Property("a", DataType.string)
|
|
assert _enforce_property(data, prop) == data
|
|
|
|
data = ["some_sentence1", "some_sentence2"]
|
|
prop = Property("a", Array(DataType.string))
|
|
assert _enforce_property(data, prop) == data
|
|
|
|
prop = Property("a", Array(DataType.binary))
|
|
assert _enforce_property(data, prop) == [b"some_sentence1", b"some_sentence2"]
|
|
|
|
data = np.array([np.int32(1), np.int32(2)])
|
|
prop = Property("a", Array(DataType.integer))
|
|
assert (_enforce_property(data, prop) == data).all()
|
|
|
|
data = {
|
|
"a": "some_sentence",
|
|
"b": b"some_bytes",
|
|
"c": ["sentence1", "sentence2"],
|
|
"d": {"str": "value", "arr": [0.1, 0.2]},
|
|
}
|
|
prop = Property(
|
|
"any_name",
|
|
Object([
|
|
Property("a", DataType.string),
|
|
Property("b", DataType.binary, required=False),
|
|
Property("c", Array(DataType.string), required=False),
|
|
Property(
|
|
"d",
|
|
Object([
|
|
Property("str", DataType.string),
|
|
Property("arr", Array(DataType.double), required=False),
|
|
]),
|
|
),
|
|
]),
|
|
)
|
|
assert _enforce_property(data, prop) == data
|
|
data = {"a": "some_sentence", "d": {"str": "some_value"}}
|
|
assert _enforce_property(data, prop) == data
|
|
|
|
|
|
def test_enforce_property_with_errors():
|
|
with pytest.raises(
|
|
MlflowException, match=r"Failed to enforce schema of data `123` with dtype `string`"
|
|
):
|
|
_enforce_property(123, Property("a", DataType.string))
|
|
|
|
with pytest.raises(MlflowException, match=r"Missing required properties: {'a'}"):
|
|
_enforce_property(
|
|
{"b": ["some_sentence1", "some_sentence2"]},
|
|
Property(
|
|
"any_name",
|
|
Object([Property("a", DataType.string), Property("b", Array(DataType.string))]),
|
|
),
|
|
)
|
|
|
|
with pytest.raises(
|
|
MlflowException,
|
|
match=r"Failed to enforce schema for key `a`. Expected type string, received type list",
|
|
):
|
|
_enforce_property(
|
|
{"a": ["some_sentence1", "some_sentence2"]},
|
|
Property("any_name", Object([Property("a", DataType.string)])),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("data", "schema"),
|
|
[
|
|
# 1. Flat list
|
|
(["some_sentence1", "some_sentence2"], Array(DataType.string)),
|
|
# 2. Nested list
|
|
(
|
|
[
|
|
[["a", "b"], ["c", "d"]],
|
|
[["e", "f", "g"], ["h"]],
|
|
[[]],
|
|
],
|
|
Array(Array(Array(DataType.string))),
|
|
),
|
|
# 3. Array of Object
|
|
(
|
|
[
|
|
{"a": "some_sentence1", "b": "some_sentence2"},
|
|
{"a": "some_sentence3", "c": ["some_sentence4", "some_sentence5"]},
|
|
],
|
|
Array(
|
|
Object([
|
|
Property("a", DataType.string),
|
|
Property("b", DataType.string, required=False),
|
|
Property("c", Array(DataType.string), required=False),
|
|
])
|
|
),
|
|
),
|
|
# 4. Empty list
|
|
([], Array(DataType.string)),
|
|
],
|
|
)
|
|
def test_enforce_array_on_list(data, schema):
|
|
assert _enforce_array(data, schema) == data
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("data", "schema"),
|
|
[
|
|
# 1. 1D array
|
|
(np.array(["some_sentence1", "some_sentence2"]), Array(DataType.string)),
|
|
# 2. 2D array
|
|
(
|
|
np.array([
|
|
["a", "b"],
|
|
["c", "d"],
|
|
]),
|
|
Array(Array(DataType.string)),
|
|
),
|
|
# 3. Empty array
|
|
(np.array([[], []]), Array(Array(DataType.string))),
|
|
],
|
|
)
|
|
def test_enforce_array_on_numpy_array(data, schema):
|
|
assert (_enforce_array(data, schema) == data).all()
|
|
|
|
|
|
def test_enforce_array_with_errors():
|
|
with pytest.raises(MlflowException, match=r"Expected data to be list or numpy array, got str"):
|
|
_enforce_array("abc", Array(DataType.string))
|
|
|
|
with pytest.raises(MlflowException, match=r"Incompatible input types"):
|
|
_enforce_array([123, 456, 789], Array(DataType.string))
|
|
|
|
# Nested array with mixed type elements
|
|
with pytest.raises(MlflowException, match=r"Incompatible input types"):
|
|
_enforce_array([["a", "b"], [1, 2]], Array(Array(DataType.string)))
|
|
|
|
# Nested array with different nest level
|
|
with pytest.raises(MlflowException, match=r"Expected data to be list or numpy array, got str"):
|
|
_enforce_array([["a", "b"], "c"], Array(Array(DataType.string)))
|
|
|
|
# Missing priperties in Object
|
|
with pytest.raises(MlflowException, match=r"Missing required properties: {'b'}"):
|
|
_enforce_array(
|
|
[
|
|
{"a": "some_sentence1", "b": "some_sentence2"},
|
|
{"a": "some_sentence3", "c": ["some_sentence4", "some_sentence5"]},
|
|
],
|
|
Array(Object([Property("a", DataType.string), Property("b", DataType.string)])),
|
|
)
|
|
|
|
# Extra properties
|
|
with pytest.raises(
|
|
MlflowException, match=r"Invalid properties not defined in the schema found: {'c'}"
|
|
):
|
|
_enforce_array(
|
|
[
|
|
{"a": "some_sentence1", "b": "some_sentence2"},
|
|
{"a": "some_sentence3", "c": ["some_sentence4", "some_sentence5"]},
|
|
],
|
|
Array(
|
|
Object([
|
|
Property("a", DataType.string),
|
|
Property("b", DataType.string, required=False),
|
|
])
|
|
),
|
|
)
|
|
|
|
|
|
def test_model_code_validation():
|
|
# Invalid code with dbutils
|
|
invalid_code = "dbutils.library.restartPython()\nsome_python_variable = 5"
|
|
|
|
with mock.patch("mlflow.models.utils._logger.warning") as mock_warning:
|
|
_validate_model_code_from_notebook(invalid_code)
|
|
mock_warning.assert_called_once_with(
|
|
"The model file uses 'dbutils' commands which are not supported. To ensure your "
|
|
"code functions correctly, make sure that it does not rely on these dbutils "
|
|
"commands for correctness."
|
|
)
|
|
|
|
# Code with commented magic commands displays warning
|
|
warning_code = "# dbutils.library.restartPython()\n# MAGIC %run ../wheel_installer"
|
|
|
|
with mock.patch("mlflow.models.utils._logger.warning") as mock_warning:
|
|
_validate_model_code_from_notebook(warning_code)
|
|
mock_warning.assert_called_once_with(
|
|
"The model file uses magic commands which have been commented out. To ensure your code "
|
|
"functions correctly, make sure that it does not rely on these magic commands for "
|
|
"correctness."
|
|
)
|
|
|
|
# Code with commented pip magic commands does not warn
|
|
warning_code = "# MAGIC %pip install mlflow"
|
|
with mock.patch("mlflow.models.utils._logger.warning") as mock_warning:
|
|
_validate_model_code_from_notebook(warning_code)
|
|
mock_warning.assert_not_called()
|
|
|
|
# Test valid code
|
|
valid_code = "some_valid_python_code = 'valid'"
|
|
|
|
validated_code = _validate_model_code_from_notebook(valid_code).decode("utf-8")
|
|
assert validated_code == valid_code
|
|
|
|
# Test uncommented magic commands
|
|
code_with_magic_command = (
|
|
"valid_python_code = 'valid'\n%pip install sqlparse\nvalid_python_code = 'valid'\n# Comment"
|
|
)
|
|
expected_validated_code = (
|
|
"valid_python_code = 'valid'\n# MAGIC %pip install sqlparse\nvalid_python_code = "
|
|
"'valid'\n# Comment"
|
|
)
|
|
|
|
validated_code_with_magic_command = _validate_model_code_from_notebook(
|
|
code_with_magic_command
|
|
).decode("utf-8")
|
|
assert validated_code_with_magic_command == expected_validated_code
|
|
|
|
|
|
def test_config_context():
|
|
with _config_context("tests/langchain/config.yml"):
|
|
assert mlflow.models.model_config.__mlflow_model_config__ == "tests/langchain/config.yml"
|
|
|
|
assert mlflow.models.model_config.__mlflow_model_config__ is None
|
|
|
|
|
|
def test_flatten_nested_params():
|
|
nested_params = {
|
|
"a": 1,
|
|
"b": {"c": 2, "d": {"e": 3}},
|
|
"f": {"g": {"h": 4}},
|
|
}
|
|
expected_flattened_params = {
|
|
"a": 1,
|
|
"b.c": 2,
|
|
"b.d.e": 3,
|
|
"f.g.h": 4,
|
|
}
|
|
assert _flatten_nested_params(nested_params, sep=".") == expected_flattened_params
|
|
assert _flatten_nested_params(nested_params, sep="/") == {
|
|
"a": 1,
|
|
"b/c": 2,
|
|
"b/d/e": 3,
|
|
"f/g/h": 4,
|
|
}
|
|
assert _flatten_nested_params({}) == {}
|
|
|
|
params = {"a": 1, "b": 2, "c": 3}
|
|
assert _flatten_nested_params(params) == params
|
|
|
|
params = {
|
|
"a": 1,
|
|
"b": {"c": 2, "d": {"e": 3, "f": [1, 2, 3]}, "g": "hello"},
|
|
"h": {"i": None},
|
|
}
|
|
expected_flattened_params = {
|
|
"a": 1,
|
|
"b/c": 2,
|
|
"b/d/e": 3,
|
|
"b/d/f": [1, 2, 3],
|
|
"b/g": "hello",
|
|
"h/i": None,
|
|
}
|
|
assert _flatten_nested_params(params) == expected_flattened_params
|
|
|
|
nested_params = {1: {2: {3: 4}}, "a": {"b": {"c": 5}}}
|
|
expected_flattened_params_mixed = {
|
|
"1/2/3": 4,
|
|
"a/b/c": 5,
|
|
}
|
|
assert _flatten_nested_params(nested_params) == expected_flattened_params_mixed
|
|
|
|
rag_params = {
|
|
"workspace_url": "https://e2-dogfood.staging.cloud.databricks.com",
|
|
"vector_search_endpoint_name": "dbdemos_vs_endpoint",
|
|
"vector_search_index": "monitoring.rag.databricks_docs_index",
|
|
"embedding_model_endpoint_name": "databricks-bge-large-en",
|
|
"embedding_model_query_instructions": "Represent this sentence for searching",
|
|
"llm_model": "databricks-dbrx-instruct",
|
|
"llm_prompt_template": "You are a trustful assistant for Databricks users.",
|
|
"retriever_config": {"k": 5, "use_mmr": "false"},
|
|
"llm_parameters": {"temperature": 0.01, "max_tokens": 200},
|
|
"llm_prompt_template_variables": ["chat_history", "context", "question"],
|
|
"secret_scope": "dbdemos",
|
|
"secret_key": "rag_sunish",
|
|
}
|
|
|
|
expected_rag_flattened_params = {
|
|
"workspace_url": "https://e2-dogfood.staging.cloud.databricks.com",
|
|
"vector_search_endpoint_name": "dbdemos_vs_endpoint",
|
|
"vector_search_index": "monitoring.rag.databricks_docs_index",
|
|
"embedding_model_endpoint_name": "databricks-bge-large-en",
|
|
"embedding_model_query_instructions": "Represent this sentence for searching",
|
|
"llm_model": "databricks-dbrx-instruct",
|
|
"llm_prompt_template": "You are a trustful assistant for Databricks users.",
|
|
"retriever_config/k": 5,
|
|
"retriever_config/use_mmr": "false",
|
|
"llm_parameters/temperature": 0.01,
|
|
"llm_parameters/max_tokens": 200,
|
|
"llm_prompt_template_variables": ["chat_history", "context", "question"],
|
|
"secret_scope": "dbdemos",
|
|
"secret_key": "rag_sunish",
|
|
}
|
|
|
|
assert _flatten_nested_params(rag_params) == expected_rag_flattened_params
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("data", "target", "target_type"),
|
|
[
|
|
(pd.DataFrame([{"a": [1, 2, 3]}]), [{"a": [1, 2, 3]}], list),
|
|
(pd.DataFrame([{"a": np.array([1, 2, 3])}]), [{"a": [1, 2, 3]}], list),
|
|
(pd.DataFrame([{0: np.array(["abc"])[0]}]), ["abc"], list),
|
|
(np.array([1, 2, 3]), [1, 2, 3], list),
|
|
(np.array([123])[0], 123, int),
|
|
(np.array(["abc"])[0], "abc", str),
|
|
],
|
|
)
|
|
def test_convert_llm_input_data(data, target, target_type):
|
|
result = _convert_llm_input_data(data)
|
|
assert result == target
|
|
assert type(result) == target_type
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model_path", "error_message"),
|
|
[
|
|
(
|
|
"model.py",
|
|
f"The provided model path '{os.getcwd()}/model.py' does not exist. "
|
|
"Ensure the file path is valid and try again.",
|
|
),
|
|
(
|
|
"model",
|
|
f"The provided model path '{os.getcwd()}/model' does not exist. "
|
|
"Ensure the file path is valid and try again. "
|
|
f"Perhaps you meant '{os.getcwd()}/model.py'?",
|
|
),
|
|
],
|
|
)
|
|
def test_validate_and_get_model_code_path_not_found(model_path, error_message, tmp_path):
|
|
with pytest.raises(MlflowException, match=error_message):
|
|
_validate_and_get_model_code_path(model_path, tmp_path)
|
|
|
|
|
|
def test_validate_and_get_model_code_path_success(tmp_path):
|
|
# if the model file exists, return the path as is
|
|
model_path = os.path.abspath(__file__)
|
|
actual = _validate_and_get_model_code_path(model_path, tmp_path)
|
|
|
|
assert actual == model_path
|
|
|
|
|
|
def test_suppress_schema_error(monkeypatch):
|
|
schema = Schema([
|
|
ColSpec("double", "id"),
|
|
ColSpec("string", "name"),
|
|
])
|
|
monkeypatch.setenv(MLFLOW_DISABLE_SCHEMA_DETAILS.name, "true")
|
|
data = pd.DataFrame({"id": [1, 2]}, dtype="float64")
|
|
|
|
with pytest.raises(
|
|
MlflowException,
|
|
match=r"Failed to enforce model input schema. Please check your input data.",
|
|
):
|
|
_validate_prediction_input(data, None, schema, None)
|
|
|
|
|
|
def test_enforce_schema_with_missing_and_extra_columns(monkeypatch):
|
|
schema = Schema([
|
|
ColSpec("long", "id"),
|
|
ColSpec("string", "name"),
|
|
])
|
|
monkeypatch.setenv(MLFLOW_DISABLE_SCHEMA_DETAILS.name, "true")
|
|
input_data = pd.DataFrame({"id": [1, 2], "extra_col": ["mlflow", "oss"]})
|
|
with pytest.raises(
|
|
MlflowException, match=r"Input schema validation failed.*extra inputs provided"
|
|
):
|
|
_enforce_schema(input_data, schema)
|