Repository: mlflow/mlflow

Issue:
[BUG] mlflow.pyfunc.get_model_dependencies raises exception when using `model:/<model_name>/<version>`
### Willingness to contribute

Yes. I would be willing to contribute a fix for this bug with guidance from the MLflow community.

### System information

- **Have I written custom code (as opposed to using a stock example script provided in MLflow)**: custom code
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**:  docker container from  continuumio/miniconda3:4.11.0
- **MLflow installed from (source or binary)**: pip install mlflow
- **MLflow version (run ``mlflow --version``)**:  mlflow, version 1.25.1
- **Python version**:  Python 3.9.12
- **npm version, if running the dev UI**:


### Describe the problem

`mlflow.pyfunc.get_model_dependencies` raises an exception when using a model uri like `models:/<model_name>/<version>`.   If I use the original run like `run:/<run-id>/path/to/model` it works as expected.

### Tracking information

_No response_

### Code to reproduce issue

```python
import mlflow
logged_model = 'models:/MyModel/1'
deps = mlflow.pyfunc.get_model_dependencies(logged_model, format="conda")
```

### Other info / logs

```
Traceback (most recent call last):
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/utils/model_utils.py", line 57, in _get_flavor_configuration_from_uri
    ml_model_file = _download_artifact_from_uri(
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/tracking/artifact_utils.py", line 95, in _download_artifact_from_uri
    return get_artifact_repository(artifact_uri=root_uri).download_artifacts(
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/store/artifact/artifact_repository_registry.py", line 107, in get_artifact_repository
    return _artifact_repository_registry.get_artifact_repository(artifact_uri)
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/store/artifact/artifact_repository_registry.py", line 73, in get_artifact_repository
    return repository(artifact_uri)
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/store/artifact/models_artifact_repo.py", line 35, in __init__
    uri = ModelsArtifactRepository.get_underlying_uri(artifact_uri)
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/store/artifact/models_artifact_repo.py", line 54, in get_underlying_uri
    (name, version) = get_model_name_and_version(client, uri)
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/store/artifact/utils/models.py", line 71, in get_model_name_and_version
    (model_name, model_version, model_stage) = _parse_model_uri(models_uri)
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/store/artifact/utils/models.py", line 57, in _parse_model_uri
    raise MlflowException(_improper_model_uri_msg(uri))
mlflow.exceptions.MlflowException: Not a proper models:/ URI: models:/MyModel/1/MLmodel. Models URIs must be of the form 'models:/<model_name>/suffix' where suffix is a model version, stage, or the string 'latest'.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/workspaces/outrider-mlflow/examples/model_inference/get_model_env.py", line 25, in <module>
    deps = mlflow.pyfunc.get_model_dependencies(logged_model, format="conda")
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/pyfunc/__init__.py", line 808, in get_model_dependencies
    dep_file = _get_model_dependencies(model_uri, format)
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/pyfunc/__init__.py", line 786, in _get_model_dependencies
    conda_yml_path = _download_model_conda_env(model_uri)
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/pyfunc/__init__.py", line 738, in _download_model_conda_env
    conda_yml_file_name = _get_flavor_configuration_from_uri(model_uri, FLAVOR_NAME)[ENV]
  File "/home/dev/.conda/envs/mlflow-model-inference/lib/python3.9/site-packages/mlflow/utils/model_utils.py", line 61, in _get_flavor_configuration_from_uri
    raise MlflowException(
mlflow.exceptions.MlflowException: Failed to download an "MLmodel" model file from "models:/MyModel/1": Not a proper models:/ URI: models:/MyModel/1/MLmodel. Models URIs must be of the form 'models:/<model_name>/suffix' where suffix is a model version, stage, or the string 'latest'.
```

### What component(s) does this bug affect?

- [ ] `area/artifacts`: Artifact stores and artifact logging
- [ ] `area/build`: Build and test infrastructure for MLflow
- [ ] `area/docs`: MLflow documentation pages
- [ ] `area/examples`: Example code
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for Model Registry
- [ ] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: MLflow Model server, model deployment tools, Spark UDFs
- [ ] `area/server-infra`: MLflow Tracking server backend
- [ ] `area/tracking`: Tracking Service, tracking client APIs, autologging

### What interface(s) does this bug affect?

- [ ] `area/uiux`: Front-end, user experience, plotting, JavaScript, JavaScript dev server
- [ ] `area/docker`: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
- [ ] `area/sqlalchemy`: Use of SQLAlchemy in the Tracking Service or Model Registry
- [ ] `area/windows`: Windows support

### What language(s) does this bug affect?

- [ ] `language/r`: R APIs and clients
- [ ] `language/java`: Java APIs and clients
- [ ] `language/new`: Proposals for new client languages

### What integration(s) does this bug affect?

- [ ] `integrations/azure`: Azure and Azure ML integrations
- [ ] `integrations/sagemaker`: SageMaker integrations
- [ ] `integrations/databricks`: Databricks integrations


Discussion:
When trying to fetch the model configuration file in [_get_flavor_configuration_from_uri](https://github.com/mlflow/mlflow/blob/c1722fd13aa0428b64ff0575c24a43efef0c9bdc/mlflow/utils/model_utils.py#L45) `MLmodel` is appended to the model uri.  Because of [this check](https://github.com/mlflow/mlflow/blob/master/mlflow/tracking/artifact_utils.py#L87) in `_download_artifact_from_uri` the model uri plus artifact path are passed through to `_parse_model_uri` causing the error. 
@lineback I was able to reproduce the issue. Looks like we need to resolve `models:/MyModel/1` before appending `MLmodel`.
@lineback Would you be interested in fixing this issue?
cc @WeichenXu123 
@harupy yes, I can work on it.
@lineback Looking forward to your PR!

Resolving patch:
diff --git a/mlflow/store/artifact/models_artifact_repo.py b/mlflow/store/artifact/models_artifact_repo.py
--- a/mlflow/store/artifact/models_artifact_repo.py
+++ b/mlflow/store/artifact/models_artifact_repo.py
@@ -41,6 +41,20 @@ def __init__(self, artifact_uri):
     def is_models_uri(uri):
         return urllib.parse.urlparse(uri).scheme == "models"
 
+    @staticmethod
+    def split_models_uri(uri):
+        """
+        Split 'models:/<name>/<version>/path/to/model' into
+        ('models:/<name>/<version>', 'path/to/model').
+        """
+        path = urllib.parse.urlparse(uri).path
+        if path.count("/") >= 3 and not path.endswith("/"):
+            splits = path.split("/", 3)
+            model_name_and_version = splits[:3]
+            artifact_path = splits[-1]
+            return "models:" + "/".join(model_name_and_version), artifact_path
+        return uri, ""
+
     @staticmethod
     def get_underlying_uri(uri):
         # Note: to support a registry URI that is different from the tracking URI here,
diff --git a/mlflow/tracking/artifact_utils.py b/mlflow/tracking/artifact_utils.py
--- a/mlflow/tracking/artifact_utils.py
+++ b/mlflow/tracking/artifact_utils.py
@@ -85,8 +85,7 @@ def _download_artifact_from_uri(artifact_uri, output_path=None):
     # For models:/ URIs, it doesn't make sense to initialize a ModelsArtifactRepository with only
     # the model name portion of the URI, then call download_artifacts with the version info.
     if ModelsArtifactRepository.is_models_uri(artifact_uri):
-        root_uri = artifact_uri
-        artifact_path = ""
+        root_uri, artifact_path = ModelsArtifactRepository.split_models_uri(artifact_uri)
     else:
         artifact_path = posixpath.basename(parsed_uri.path)
         parsed_uri = parsed_uri._replace(path=posixpath.dirname(parsed_uri.path))


Test patch:
diff --git a/tests/pyfunc/test_dependencies_functions.py b/tests/pyfunc/test_dependencies_functions.py
--- a/tests/pyfunc/test_dependencies_functions.py
+++ b/tests/pyfunc/test_dependencies_functions.py
@@ -2,6 +2,7 @@
 import pytest
 import cloudpickle
 import sklearn
+from sklearn.linear_model import LinearRegression
 from pathlib import Path
 
 from mlflow.pyfunc import _warn_dependency_requirement_mismatches, get_model_dependencies
@@ -216,3 +217,11 @@ def test_get_model_dependencies_read_conda_file(tmp_path):
 
     with pytest.raises(MlflowException, match="No pip section found in conda.yaml file"):
         get_model_dependencies(model_path, format="pip")
+
+
+def test_get_model_dependencies_with_model_version_uri():
+    with mlflow.start_run():
+        mlflow.sklearn.log_model(LinearRegression(), "model", registered_model_name="linear")
+
+    deps = get_model_dependencies("models:/linear/1", format="pip")
+    assert f"scikit-learn=={sklearn.__version__}" in Path(deps).read_text()
diff --git a/tests/store/artifact/test_models_artifact_repo.py b/tests/store/artifact/test_models_artifact_repo.py
--- a/tests/store/artifact/test_models_artifact_repo.py
+++ b/tests/store/artifact/test_models_artifact_repo.py
@@ -105,3 +105,15 @@ def test_models_artifact_repo_uses_repo_download_artifacts():
         models_repo.repo = Mock()
         models_repo.download_artifacts("artifact_path", "dst_path")
         models_repo.repo.download_artifacts.assert_called_once()
+
+
+def test_split_models_uri():
+    assert ModelsArtifactRepository.split_models_uri("models:/model/1") == ("models:/model/1", "")
+    assert ModelsArtifactRepository.split_models_uri("models:/model/1/path") == (
+        "models:/model/1",
+        "path",
+    )
+    assert ModelsArtifactRepository.split_models_uri("models:/model/1/path/to/artifact") == (
+        "models:/model/1",
+        "path/to/artifact",
+    )