Files
2026-07-13 13:22:34 +08:00

184 lines
8.4 KiB
Python

"""
Utilities for dealing with artifacts in the context of a Run.
"""
import os
import pathlib
import posixpath
import tempfile
import urllib.parse
import uuid
from typing import Any
from mlflow.exceptions import MlflowException
from mlflow.protos.databricks_pb2 import INVALID_PARAMETER_VALUE
from mlflow.store.artifact.artifact_repository_registry import get_artifact_repository
from mlflow.store.artifact.dbfs_artifact_repo import DbfsRestArtifactRepository
from mlflow.store.artifact.models_artifact_repo import ModelsArtifactRepository
from mlflow.tracking._tracking_service.utils import _get_store
from mlflow.utils.file_utils import path_to_local_file_uri
from mlflow.utils.os import is_windows
from mlflow.utils.uri import add_databricks_profile_info_to_artifact_uri, append_to_uri_path
def get_artifact_uri(run_id, artifact_path=None, tracking_uri=None):
"""Get the absolute URI of the specified artifact in the specified run. If `path` is not
specified the artifact root URI of the specified run will be returned; calls to ``log_artifact``
and ``log_artifacts`` write artifact(s) to subdirectories of the artifact root URI.
Args:
run_id: The ID of the run for which to obtain an absolute artifact URI.
artifact_path: The run-relative artifact path. For example,
``path/to/artifact``. If unspecified, the artifact root URI for the
specified run will be returned.
tracking_uri: The tracking URI from which to get the run and its artifact location. If
not given, the current default tracking URI is used.
Returns:
An *absolute* URI referring to the specified artifact or the specified run's artifact
root. For example, if an artifact path is provided and the specified run uses an
S3-backed store, this may be a uri of the form
``s3://<bucket_name>/path/to/artifact/root/path/to/artifact``. If an artifact path
is not provided and the specified run uses an S3-backed store, this may be a URI of
the form ``s3://<bucket_name>/path/to/artifact/root``.
"""
if not run_id:
raise MlflowException(
message="A run_id must be specified in order to obtain an artifact uri!",
error_code=INVALID_PARAMETER_VALUE,
)
store = _get_store(tracking_uri)
run = store.get_run(run_id)
# Maybe move this method to RunsArtifactRepository so the circular dependency is clearer.
assert urllib.parse.urlparse(run.info.artifact_uri).scheme != "runs" # avoid an infinite loop
if artifact_path is None:
return run.info.artifact_uri
else:
return append_to_uri_path(run.info.artifact_uri, artifact_path)
# TODO: This would be much simpler if artifact_repo.download_artifacts could take the absolute path
# or no path.
def _get_root_uri_and_artifact_path(artifact_uri):
"""Parse the artifact_uri to get the root_uri and artifact_path.
Args:
artifact_uri: The *absolute* URI of the artifact.
"""
if os.path.exists(artifact_uri):
if not is_windows():
# If we're dealing with local files, just reference the direct pathing.
# non-nt-based file systems can directly reference path information, while nt-based
# systems need to url-encode special characters in directory listings to be able to
# resolve them (i.e., spaces converted to %20 within a file name or path listing)
root_uri = os.path.dirname(artifact_uri)
artifact_path = os.path.basename(artifact_uri)
return root_uri, artifact_path
else: # if we're dealing with nt-based systems, we need to utilize pathname2url to encode.
artifact_uri = path_to_local_file_uri(artifact_uri)
parsed_uri = urllib.parse.urlparse(str(artifact_uri))
prefix = ""
if parsed_uri.scheme and not parsed_uri.path.startswith("/"):
# relative path is a special case, urllib does not reconstruct it properly
prefix = parsed_uri.scheme + ":"
parsed_uri = parsed_uri._replace(scheme="")
# 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_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))
root_uri = prefix + urllib.parse.urlunparse(parsed_uri)
return root_uri, artifact_path
def _download_artifact_from_uri(
artifact_uri: str,
output_path: str | None = None,
lineage_header_info: dict[str, Any] | None = None,
tracking_uri: str | None = None,
registry_uri: str | None = None,
) -> str:
"""
Args:
artifact_uri: The *absolute* URI of the artifact to download.
output_path: The local filesystem path to which to download the artifact. If unspecified,
a local output path will be created.
lineage_header_info: The model lineage header info to be consumed by lineage services.
tracking_uri: The tracking URI to be used when downloading artifacts.
registry_uri: The registry URI to be used when downloading artifacts.
"""
root_uri, artifact_path = _get_root_uri_and_artifact_path(artifact_uri)
repo = get_artifact_repository(
artifact_uri=root_uri, tracking_uri=tracking_uri, registry_uri=registry_uri
)
try:
if isinstance(repo, ModelsArtifactRepository):
return repo.download_artifacts(
artifact_path=artifact_path,
dst_path=output_path,
lineage_header_info=lineage_header_info,
)
return repo.download_artifacts(artifact_path=artifact_path, dst_path=output_path)
except Exception as e:
if artifact_uri.startswith("m-"):
# When a Model ID like string is passed, suggest using 'models:/{artifact_uri}' instead.
raise MlflowException(
f"Invalid uri `{artifact_uri}` is passed. Maybe you meant 'models:/{artifact_uri}'?"
) from e
raise
def _upload_artifact_to_uri(local_path, artifact_uri):
"""Uploads a local artifact (file) to a specified URI.
Args:
local_path: The local path of the file to upload.
artifact_uri: The *absolute* URI of the path to upload the artifact to.
"""
root_uri, artifact_path = _get_root_uri_and_artifact_path(artifact_uri)
get_artifact_repository(artifact_uri=root_uri).log_artifact(local_path, artifact_path)
def _upload_artifacts_to_databricks(
source, run_id, source_host_uri=None, target_databricks_profile_uri=None
):
"""Copy the artifacts from ``source`` to the destination Databricks workspace (DBFS) given by
``databricks_profile_uri`` or the current tracking URI.
Args:
source: Source location for the artifacts to copy.
run_id: Run ID to associate the artifacts with.
source_host_uri: Specifies the source artifact's host URI (e.g. Databricks tracking URI)
if applicable. If not given, defaults to the current tracking URI.
target_databricks_profile_uri: Specifies the destination Databricks host. If not given,
defaults to the current tracking URI.
Returns:
The DBFS location in the target Databricks workspace the model files have been
uploaded to.
"""
with tempfile.TemporaryDirectory() as local_dir:
source_with_profile = add_databricks_profile_info_to_artifact_uri(source, source_host_uri)
_download_artifact_from_uri(source_with_profile, local_dir)
dest_root = "dbfs:/databricks/mlflow/tmp-external-source/"
dest_root_with_profile = add_databricks_profile_info_to_artifact_uri(
dest_root, target_databricks_profile_uri
)
dest_repo = DbfsRestArtifactRepository(dest_root_with_profile)
dest_artifact_path = run_id or uuid.uuid4().hex
# Allow uploading from the same run id multiple times by randomizing a suffix
if len(dest_repo.list_artifacts(dest_artifact_path)) > 0:
dest_artifact_path = dest_artifact_path + "-" + uuid.uuid4().hex[0:4]
dest_repo.log_artifacts(local_dir, artifact_path=dest_artifact_path)
dirname = pathlib.PurePath(source).name # innermost directory name
return posixpath.join(dest_root, dest_artifact_path, dirname) # new source