import logging from typing import Callable from mlflow.entities.logged_model_parameter import LoggedModelParameter as ModelParam from mlflow.entities.metric import Metric from mlflow.entities.model_registry import ( ModelVersion, ModelVersionDeploymentJobState, ModelVersionTag, RegisteredModel, RegisteredModelAlias, RegisteredModelDeploymentJobState, RegisteredModelTag, ) from mlflow.entities.model_registry.model_version_search import ModelVersionSearch from mlflow.entities.model_registry.registered_model_search import RegisteredModelSearch from mlflow.environment_variables import MLFLOW_USE_DATABRICKS_SDK_MODEL_ARTIFACTS_REPO_FOR_UC from mlflow.exceptions import MlflowException from mlflow.protos.databricks_uc_registry_messages_pb2 import ( EmitModelVersionLineageRequest, EmitModelVersionLineageResponse, IsDatabricksSdkModelsArtifactRepositoryEnabledRequest, IsDatabricksSdkModelsArtifactRepositoryEnabledResponse, ModelVersionLineageInfo, SseEncryptionAlgorithm, TemporaryCredentials, ) from mlflow.protos.databricks_uc_registry_messages_pb2 import ModelVersion as ProtoModelVersion from mlflow.protos.databricks_uc_registry_messages_pb2 import ( ModelVersionStatus as ProtoModelVersionStatus, ) from mlflow.protos.databricks_uc_registry_messages_pb2 import ( ModelVersionTag as ProtoModelVersionTag, ) from mlflow.protos.databricks_uc_registry_messages_pb2 import ( RegisteredModel as ProtoRegisteredModel, ) from mlflow.protos.databricks_uc_registry_messages_pb2 import ( RegisteredModelTag as ProtoRegisteredModelTag, ) from mlflow.protos.databricks_uc_registry_service_pb2 import UcModelRegistryService from mlflow.protos.unity_catalog_oss_messages_pb2 import ( TemporaryCredentials as TemporaryCredentialsOSS, ) from mlflow.store.artifact.artifact_repo import ArtifactRepository from mlflow.utils.proto_json_utils import message_to_json from mlflow.utils.rest_utils import ( _REST_API_PATH_PREFIX, call_endpoint, extract_api_info_for_service, ) _logger = logging.getLogger(__name__) _METHOD_TO_INFO = extract_api_info_for_service(UcModelRegistryService, _REST_API_PATH_PREFIX) _STRING_TO_STATUS = {k: ProtoModelVersionStatus.Value(k) for k in ProtoModelVersionStatus.keys()} _STATUS_TO_STRING = {value: key for key, value in _STRING_TO_STATUS.items()} _ACTIVE_CATALOG_QUERY = "SELECT current_catalog() AS catalog" _ACTIVE_SCHEMA_QUERY = "SELECT current_database() AS schema" def uc_model_version_status_to_string(status): return _STATUS_TO_STRING[status] def model_version_from_uc_proto(uc_proto: ProtoModelVersion) -> ModelVersion: return ModelVersion( name=uc_proto.name, version=uc_proto.version, creation_timestamp=uc_proto.creation_timestamp, last_updated_timestamp=uc_proto.last_updated_timestamp, description=uc_proto.description, user_id=uc_proto.user_id, source=uc_proto.source, run_id=uc_proto.run_id, status=uc_model_version_status_to_string(uc_proto.status), status_message=uc_proto.status_message, aliases=[alias.alias for alias in (uc_proto.aliases or [])], tags=[ModelVersionTag(key=tag.key, value=tag.value) for tag in (uc_proto.tags or [])], model_id=uc_proto.model_id, params=[ ModelParam(key=param.name, value=param.value) for param in (uc_proto.model_params or []) ], metrics=[ Metric( key=metric.key, value=metric.value, timestamp=metric.timestamp, step=metric.step, dataset_name=metric.dataset_name, dataset_digest=metric.dataset_digest, model_id=metric.model_id, run_id=metric.run_id, ) for metric in (uc_proto.model_metrics or []) ], deployment_job_state=ModelVersionDeploymentJobState.from_proto( uc_proto.deployment_job_state ), ) def model_version_search_from_uc_proto(uc_proto: ProtoModelVersion) -> ModelVersionSearch: return ModelVersionSearch( name=uc_proto.name, version=uc_proto.version, creation_timestamp=uc_proto.creation_timestamp, last_updated_timestamp=uc_proto.last_updated_timestamp, description=uc_proto.description, user_id=uc_proto.user_id, source=uc_proto.source, run_id=uc_proto.run_id, status=uc_model_version_status_to_string(uc_proto.status), status_message=uc_proto.status_message, aliases=[], tags=[], deployment_job_state=ModelVersionDeploymentJobState.from_proto( uc_proto.deployment_job_state ), ) def registered_model_from_uc_proto(uc_proto: ProtoRegisteredModel) -> RegisteredModel: return RegisteredModel( name=uc_proto.name, creation_timestamp=uc_proto.creation_timestamp, last_updated_timestamp=uc_proto.last_updated_timestamp, description=uc_proto.description, aliases=[ RegisteredModelAlias(alias=alias.alias, version=alias.version) for alias in (uc_proto.aliases or []) ], tags=[RegisteredModelTag(key=tag.key, value=tag.value) for tag in (uc_proto.tags or [])], deployment_job_id=uc_proto.deployment_job_id, deployment_job_state=RegisteredModelDeploymentJobState.to_string( uc_proto.deployment_job_state ), ) def registered_model_search_from_uc_proto(uc_proto: ProtoRegisteredModel) -> RegisteredModelSearch: return RegisteredModelSearch( name=uc_proto.name, creation_timestamp=uc_proto.creation_timestamp, last_updated_timestamp=uc_proto.last_updated_timestamp, description=uc_proto.description, aliases=[], tags=[], ) def uc_registered_model_tag_from_mlflow_tags( tags: list[RegisteredModelTag] | None, ) -> list[ProtoRegisteredModelTag]: if tags is None: return [] return [ProtoRegisteredModelTag(key=t.key, value=t.value) for t in tags] def uc_model_version_tag_from_mlflow_tags( tags: list[ModelVersionTag] | None, ) -> list[ProtoModelVersionTag]: if tags is None: return [] return [ProtoModelVersionTag(key=t.key, value=t.value) for t in tags] def get_artifact_repo_from_storage_info( storage_location: str, scoped_token: TemporaryCredentials, base_credential_refresh_def: Callable[[], TemporaryCredentials], is_oss: bool = False, ) -> ArtifactRepository: """ Get an ArtifactRepository instance capable of reading/writing to a UC model version's file storage location Args: storage_location: Storage location of the model version scoped_token: Protobuf scoped token to use to authenticate to blob storage base_credential_refresh_def: Function that returns temporary credentials for accessing blob storage. It is first used to determine the type of blob storage and to access it. It is then passed to the relevant ArtifactRepository implementation to refresh credentials as needed. is_oss: Whether the user is using the OSS version of Unity Catalog """ try: if is_oss: return _get_artifact_repo_from_storage_info_oss( storage_location=storage_location, scoped_token=scoped_token, base_credential_refresh_def=base_credential_refresh_def, ) else: return _get_artifact_repo_from_storage_info( storage_location=storage_location, scoped_token=scoped_token, base_credential_refresh_def=base_credential_refresh_def, ) except ImportError as e: raise MlflowException( "Unable to import necessary dependencies to access model version files in " "Unity Catalog. Please ensure you have the necessary dependencies installed, " "e.g. by running 'pip install mlflow[databricks]' or " "'pip install mlflow-skinny[databricks]'" ) from e def _get_artifact_repo_from_storage_info( storage_location: str, scoped_token: TemporaryCredentials, base_credential_refresh_def: Callable[[], TemporaryCredentials], ) -> ArtifactRepository: credential_type = scoped_token.WhichOneof("credentials") if credential_type == "aws_temp_credentials": # Verify upfront that boto3 is importable import boto3 # noqa: F401 from mlflow.store.artifact.optimized_s3_artifact_repo import OptimizedS3ArtifactRepository aws_creds = scoped_token.aws_temp_credentials s3_upload_extra_args = _parse_aws_sse_credential(scoped_token) def aws_credential_refresh(): new_scoped_token = base_credential_refresh_def() new_aws_creds = new_scoped_token.aws_temp_credentials new_s3_upload_extra_args = _parse_aws_sse_credential(new_scoped_token) return { "access_key_id": new_aws_creds.access_key_id, "secret_access_key": new_aws_creds.secret_access_key, "session_token": new_aws_creds.session_token, "s3_upload_extra_args": new_s3_upload_extra_args, } return OptimizedS3ArtifactRepository( artifact_uri=storage_location, access_key_id=aws_creds.access_key_id, secret_access_key=aws_creds.secret_access_key, session_token=aws_creds.session_token, credential_refresh_def=aws_credential_refresh, s3_upload_extra_args=s3_upload_extra_args, ) elif credential_type == "azure_user_delegation_sas": from azure.core.credentials import AzureSasCredential from mlflow.store.artifact.azure_data_lake_artifact_repo import ( AzureDataLakeArtifactRepository, ) sas_token = scoped_token.azure_user_delegation_sas.sas_token def azure_credential_refresh(): new_scoped_token = base_credential_refresh_def() new_sas_token = new_scoped_token.azure_user_delegation_sas.sas_token return { "credential": AzureSasCredential(new_sas_token), } return AzureDataLakeArtifactRepository( artifact_uri=storage_location, credential=AzureSasCredential(sas_token), credential_refresh_def=azure_credential_refresh, ) elif credential_type == "gcp_oauth_token": from google.cloud.storage import Client from google.oauth2.credentials import Credentials from mlflow.store.artifact.gcs_artifact_repo import GCSArtifactRepository credentials = Credentials(scoped_token.gcp_oauth_token.oauth_token) def gcp_credential_refresh(): new_scoped_token = base_credential_refresh_def() new_gcp_creds = new_scoped_token.gcp_oauth_token return { "oauth_token": new_gcp_creds.oauth_token, } client = Client(project="mlflow", credentials=credentials) return GCSArtifactRepository( artifact_uri=storage_location, client=client, credential_refresh_def=gcp_credential_refresh, ) elif credential_type == "r2_temp_credentials": from mlflow.store.artifact.r2_artifact_repo import R2ArtifactRepository r2_creds = scoped_token.r2_temp_credentials def r2_credential_refresh(): new_scoped_token = base_credential_refresh_def() new_r2_creds = new_scoped_token.r2_temp_credentials return { "access_key_id": new_r2_creds.access_key_id, "secret_access_key": new_r2_creds.secret_access_key, "session_token": new_r2_creds.session_token, } return R2ArtifactRepository( artifact_uri=storage_location, access_key_id=r2_creds.access_key_id, secret_access_key=r2_creds.secret_access_key, session_token=r2_creds.session_token, credential_refresh_def=r2_credential_refresh, ) else: raise MlflowException( f"Got unexpected credential type {credential_type} when attempting to " "access model version files in Unity Catalog. Try upgrading to the latest " "version of the MLflow Python client." ) def _get_artifact_repo_from_storage_info_oss( storage_location: str, scoped_token: TemporaryCredentialsOSS, base_credential_refresh_def: Callable[[], TemporaryCredentialsOSS], ) -> ArtifactRepository: # OSS Temp Credential doesn't have a oneof credential field # So, we must check for the individual cloud credentials if len(scoped_token.aws_temp_credentials.access_key_id) > 0: # Verify upfront that boto3 is importable import boto3 # noqa: F401 from mlflow.store.artifact.optimized_s3_artifact_repo import OptimizedS3ArtifactRepository aws_creds = scoped_token.aws_temp_credentials def aws_credential_refresh(): new_scoped_token = base_credential_refresh_def() new_aws_creds = new_scoped_token.aws_temp_credentials return { "access_key_id": new_aws_creds.access_key_id, "secret_access_key": new_aws_creds.secret_access_key, "session_token": new_aws_creds.session_token, } return OptimizedS3ArtifactRepository( artifact_uri=storage_location, access_key_id=aws_creds.access_key_id, secret_access_key=aws_creds.secret_access_key, session_token=aws_creds.session_token, credential_refresh_def=aws_credential_refresh, ) elif len(scoped_token.azure_user_delegation_sas.sas_token) > 0: from azure.core.credentials import AzureSasCredential from mlflow.store.artifact.azure_data_lake_artifact_repo import ( AzureDataLakeArtifactRepository, ) sas_token = scoped_token.azure_user_delegation_sas.sas_token def azure_credential_refresh(): new_scoped_token = base_credential_refresh_def() new_sas_token = new_scoped_token.azure_user_delegation_sas.sas_token return { "credential": AzureSasCredential(new_sas_token), } return AzureDataLakeArtifactRepository( artifact_uri=storage_location, credential=AzureSasCredential(sas_token), credential_refresh_def=azure_credential_refresh, ) elif len(scoped_token.gcp_oauth_token.oauth_token) > 0: from google.cloud.storage import Client from google.oauth2.credentials import Credentials from mlflow.store.artifact.gcs_artifact_repo import GCSArtifactRepository credentials = Credentials(scoped_token.gcp_oauth_token.oauth_token) client = Client(project="mlflow", credentials=credentials) return GCSArtifactRepository(artifact_uri=storage_location, client=client) else: raise MlflowException( "Got no credential type when attempting to " "access model version files in Unity Catalog. Try upgrading to the latest " "version of the MLflow Python client." ) def _parse_aws_sse_credential(scoped_token: TemporaryCredentials): encryption_details = scoped_token.encryption_details if not encryption_details: return {} if encryption_details.WhichOneof("encryption_details_type") != "sse_encryption_details": return {} sse_encryption_details = encryption_details.sse_encryption_details if sse_encryption_details.algorithm == SseEncryptionAlgorithm.AWS_SSE_S3: return { "ServerSideEncryption": "AES256", } if sse_encryption_details.algorithm == SseEncryptionAlgorithm.AWS_SSE_KMS: return { "ServerSideEncryption": "aws:kms", "SSEKMSKeyId": sse_encryption_details.aws_kms_key_arn, } else: return {} def get_full_name_from_sc(name, spark) -> str: """ Constructs the full name of a registered model using the active catalog and schema in a spark session / context. Args: name: The model name provided by the user. spark: The active spark session. """ num_levels = len(name.split(".")) if num_levels >= 3 or spark is None: return name catalog = spark.sql(_ACTIVE_CATALOG_QUERY).collect()[0]["catalog"] # return the user provided name if the catalog is the hive metastore default if catalog in {"spark_catalog", "hive_metastore"}: return name if num_levels == 2: return f"{catalog}.{name}" schema = spark.sql(_ACTIVE_SCHEMA_QUERY).collect()[0]["schema"] return f"{catalog}.{schema}.{name}" def is_databricks_sdk_models_artifact_repository_enabled(host_creds): # Return early if the environment variable is set to use the SDK models artifact repository if MLFLOW_USE_DATABRICKS_SDK_MODEL_ARTIFACTS_REPO_FOR_UC.defined: return MLFLOW_USE_DATABRICKS_SDK_MODEL_ARTIFACTS_REPO_FOR_UC.get() endpoint, method = _METHOD_TO_INFO[IsDatabricksSdkModelsArtifactRepositoryEnabledRequest] req_body = message_to_json(IsDatabricksSdkModelsArtifactRepositoryEnabledRequest()) response_proto = IsDatabricksSdkModelsArtifactRepositoryEnabledResponse() try: resp = call_endpoint( host_creds=host_creds, endpoint=endpoint, method=method, json_body=req_body, response_proto=response_proto, ) return resp.is_databricks_sdk_models_artifact_repository_enabled except Exception as e: _logger.warning( "Failed to confirm if DatabricksSDKModelsArtifactRepository should be used; " f"falling back to default. Error: {e}" ) return False def emit_model_version_lineage(host_creds, name, version, entities, direction): endpoint, method = _METHOD_TO_INFO[EmitModelVersionLineageRequest] req_body = message_to_json( EmitModelVersionLineageRequest( name=name, version=version, model_version_lineage_info=ModelVersionLineageInfo( entities=entities, direction=direction, ), ) ) response_proto = EmitModelVersionLineageResponse() try: call_endpoint( host_creds=host_creds, endpoint=endpoint, method=method, json_body=req_body, response_proto=response_proto, ) except Exception as e: _logger.warning(f"Failed to emit best-effort model version lineage. Error: {e}")