diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 241bb99..8321a06 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -26,13 +26,14 @@ jobs: if: hashFiles('**/requirements.txt', '**/pyproject.toml') == '' run: | touch ./requirements.txt - echo "mlflow==2.20.1" > ./requirements.txt + echo "mlflow[auth]==3.2.0" > ./requirements.txt - uses: actions/setup-python@v4 with: python-version: '3.12.3' cache: 'pip' - name: Setup mlflow locally run: | + export MLFLOW_FLASK_SERVER_SECRET_KEY='mlflowclient.jl' pip install -r ./requirements.txt python3 /opt/hostedtoolcache/Python/3.12.3/x64/bin/mlflow server --app-name basic-auth --host 0.0.0.0 --port 5000 & sleep 5 @@ -40,7 +41,7 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 + - uses: actions/cache@v4 env: cache-name: cache-artifacts with: diff --git a/README.md b/README.md index ca2ce9a..78f9e16 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build Status](https://github.com/JuliaAI/MLFlowClient.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaAI/MLFlowClient.jl/actions/workflows/CI.yml?query=branch%3Amain) [![Coverage](https://codecov.io/gh/JuliaAI/MLFlowClient.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaAI/MLFlowClient.jl) -Julia client for [MLFlow](https://www.mlflow.org/) `2.20.1` (but should work with other versions as well). +Julia client for [MLFlow](https://www.mlflow.org/) `3.2.0` (but should work with other versions as well). - [x] Supports tracking of metrics, parameters, tags, artifacts, and models. - [x] Compatible with latest MLFlow server capabilities. diff --git a/docs/src/reference/logger.md b/docs/src/reference/logger.md index 2bad6ce..f27b233 100644 --- a/docs/src/reference/logger.md +++ b/docs/src/reference/logger.md @@ -4,4 +4,5 @@ logmetric logbatch loginputs logparam +logmodel ``` diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md index b6782ba..86cdf93 100644 --- a/docs/src/reference/types.md +++ b/docs/src/reference/types.md @@ -2,9 +2,6 @@ ```@docs MLFlow Tag -ViewType -RunStatus -ModelVersionStatus Dataset DatasetInput FileInfo @@ -19,7 +16,6 @@ RunData RunInfo RunInputs User -Permission ExperimentPermission RegisteredModelPermission ``` diff --git a/src/MLFlowClient.jl b/src/MLFlowClient.jl index 92a41c8..1e50aac 100644 --- a/src/MLFlowClient.jl +++ b/src/MLFlowClient.jl @@ -26,7 +26,7 @@ include("types/tag.jl") export Tag include("types/enums.jl") -export ViewType, RunStatus, ModelVersionStatus, Permission +export ViewType, RunStatus, ModelVersionStatus, Permission, DeploymentJobRunState, State include("types/dataset.jl") export Dataset, DatasetInput @@ -34,8 +34,9 @@ export Dataset, DatasetInput include("types/artifact.jl") export FileInfo -include("types/model_version.jl") -export ModelVersion +include("types/model.jl") +export ModelInput, ModelMetric, ModelOutput, ModelParam, ModelVersion, + ModelVersionDeploymentJobState include("types/registered_model.jl") export RegisteredModel, RegisteredModelAlias, RegisteredModelPermission @@ -64,7 +65,7 @@ export getrun, createrun, deleterun, setruntag, updaterun, restorerun, searchrun deleteruntag include("services/logger.jl") -export logbatch, loginputs, logmetric, logparam +export logbatch, loginputs, logmetric, logmodel, logparam include("services/artifact.jl") export listartifacts diff --git a/src/services/experiment.jl b/src/services/experiment.jl index f321116..535b140 100644 --- a/src/services/experiment.jl +++ b/src/services/experiment.jl @@ -156,7 +156,7 @@ updateexperiment(instance::MLFlow, experiment::Experiment, new_name::String)::Bo - `order_by`: List of columns for ordering search results, which can include [`Experiment`](@ref) name and id with an optional “DESC” or “ASC” annotation, where “ASC” is the default. - `view_type`: Qualifier for type of experiments to be returned. If unspecified, return - only active experiments. For more values, see [`ViewType`](@ref). + only active experiments. # Returns - Vector of [`Experiment`](@ref) that were found in the [`MLFlow`](@ref) instance. @@ -164,7 +164,7 @@ updateexperiment(instance::MLFlow, experiment::Experiment, new_name::String)::Bo """ function searchexperiments(instance::MLFlow; max_results::Int64=20000, page_token::String="", filter::String="", order_by::Array{String}=String[], - view_type::ViewType=ACTIVE_ONLY)::Tuple{Array{Experiment},Union{String,Nothing}} + view_type::ViewType.ViewTypeEnum=ViewType.ACTIVE_ONLY)::Tuple{Array{Experiment},Union{String,Nothing}} parameters = (; max_results, page_token, filter, :view_type => view_type |> Integer) if order_by |> !isempty @@ -220,22 +220,22 @@ setexperimenttag(instance::MLFlow, experiment::Experiment, key::String, - `instance`: [`MLFlow`](@ref) configuration. - `experiment_id`: [`Experiment`](@ref) id. - `username`: [`User`](@ref) username. -- `permission`: [`Permission`](@ref) to grant. +- `permission`: Permission to grant. # Returns An instance of type [`ExperimentPermission`](@ref). """ function createexperimentpermission(instance::MLFlow, experiment_id::String, - username::String, permission::Permission)::ExperimentPermission + username::String, permission::Permission.PermissionEnum)::ExperimentPermission result = mlfpost(instance, "experiments/permissions/create"; experiment_id=experiment_id, username=username, permission=permission) return result["experiment_permission"] |> ExperimentPermission end createexperimentpermission(instance::MLFlow, experiment_id::Integer, - username::String, permission::Permission)::ExperimentPermission = + username::String, permission::Permission.PermissionEnum)::ExperimentPermission = createexperimentpermission(instance, experiment_id |> string, username, permission) createexperimentpermission(instance::MLFlow, experiment::Experiment, - username::String, permission::Permission)::ExperimentPermission = + username::String, permission::Permission.PermissionEnum)::ExperimentPermission = createexperimentpermission(instance, experiment.experiment_id, username, permission) """ @@ -276,22 +276,22 @@ getexperimentpermission(instance::MLFlow, experiment::Experiment, - `instance`: [`MLFlow`](@ref) configuration. - `experiment_id`: [`Experiment`](@ref) id. - `username`: [`User`](@ref) username. -- `permission`: [`Permission`](@ref) to grant. +- `permission`: Permission to grant. # Returns `true` if successful. Otherwise, raises exception. """ function updateexperimentpermission(instance::MLFlow, experiment_id::String, - username::String, permission::Permission)::Bool + username::String, permission::Permission.PermissionEnum)::Bool mlfpatch(instance, "experiments/permissions/update"; experiment_id=experiment_id, username=username, permission=permission) return true end updateexperimentpermission(instance::MLFlow, experiment_id::Integer, - username::String, permission::Permission)::Bool = + username::String, permission::Permission.PermissionEnum)::Bool = updateexperimentpermission(instance, experiment_id |> string, username, permission) updateexperimentpermission(instance::MLFlow, experiment::Experiment, - username::String, permission::Permission)::Bool = + username::String, permission::Permission.PermissionEnum)::Bool = updateexperimentpermission(instance, experiment.experiment_id, username, permission) """ diff --git a/src/services/logger.jl b/src/services/logger.jl index 43c2a39..45c2a82 100644 --- a/src/services/logger.jl +++ b/src/services/logger.jl @@ -57,8 +57,9 @@ For more information about this function, check [MLFlow official documentation]( - `params`: A collection of [`Param`](@ref) to log. - `tags`: A collection of [`Tag`](@ref) to log. -**Note**: A single request can contain up to 1000 metrics, and up to 1000 metrics, params, -and tags in total. +!!! note + A single request can contain up to 1000 metrics, and up to 1000 metrics, params, and + tags in total. # Returns `true` if successful. Otherwise, raises exception. @@ -123,3 +124,18 @@ logparam(instance::MLFlow, run_id::String, param::Param)::Bool = logparam(instance, run_id, param.key, param.value) logparam(instance::MLFlow, run::Run, param::Param)::Bool = logparam(instance, run.info.run_id, param.key, param.value) + +""" + logmodel(instance::MLFlow, run_id::String, model_json::String) + +# Arguments +- `instance`: [`MLFlow`](@ref) configuration. +- `run_id`: ID of the [`Run`](@ref) to log under. +- `model_json`: MLmodel file in json format. +""" +function logmodel(instance::MLFlow, run_id::String, model_json::String)::Bool + mlfpost(instance, "runs/log-model"; run_id=run_id, model_json=model_json) + return true +end +logmodel(instance::MLFlow, run::Run, model_json::String)::Bool = + logmodel(instance, run.info.run_id, model_json) diff --git a/src/services/misc.jl b/src/services/misc.jl index c7a023a..4b0c5d7 100644 --- a/src/services/misc.jl +++ b/src/services/misc.jl @@ -10,7 +10,7 @@ Get a list of all values for the specified [`Metric`](@ref) for a given [`Run`]( - `metric_key`: Name of the [`Metric`](@ref) to fetch. - `page_token`: Token indicating the page of [`Metric`](@ref) history to fetch. - `max_results`: Maximum number of logged instances of a [`Metric`](@ref) for a - [`Run`](@ref) to return per call. + [`Run`](@ref) to return per call. Defaults to 50. # Returns - A list of all historical values for the specified [`Metric`](@ref) in the specified @@ -18,25 +18,23 @@ Get a list of all values for the specified [`Metric`](@ref) for a given [`Run`]( - The next page token if there are more results. """ function getmetrichistory(instance::MLFlow, run_id::String, metric_key::String; - page_token::String="", max_results::Union{Int64,Missing}=missing + page_token::Union{String,Missing}=missing, max_results::Int64=50 )::Tuple{Array{Metric},Union{String,Nothing}} result = mlfget(instance, "metrics/get-history"; run_id=run_id, metric_key=metric_key, page_token=page_token, max_results=(ismissing(max_results) ? max_results : (max_results |> Int32))) - metrics = result["metrics"] |> (x -> [Metric(y) for y in x]) + metrics = get(result, "metrics", []) |> (x -> [Metric(y) for y in x]) next_page_token = get(result, "next_page_token", nothing) return metrics, next_page_token end getmetrichistory(instance::MLFlow, run::Run, metric_key::String; page_token::String="", - max_results::Union{Int64,Missing}=missing -)::Tuple{Array{Metric},Union{String,Nothing}} = + max_results::Int64=50)::Tuple{Array{Metric},Union{String,Nothing}} = getmetrichistory(instance, run.info.run_id, metric_key; page_token=page_token, max_results=max_results) getmetrichistory(instance::MLFlow, run::Run, metric::Metric; page_token::String="", - max_results::Union{Int64,Missing}=missing -)::Tuple{Array{Metric},Union{String,Nothing}} = + max_results::Int64=50)::Tuple{Array{Metric},Union{String,Nothing}} = getmetrichistory(instance, run.info.run_id, metric.key; page_token=page_token, max_results=max_results) diff --git a/src/services/registered_model.jl b/src/services/registered_model.jl index e7b99df..9f7330b 100644 --- a/src/services/registered_model.jl +++ b/src/services/registered_model.jl @@ -17,10 +17,11 @@ name already exists. An instance of type [`RegisteredModel`](@ref). """ function createregisteredmodel(instance::MLFlow, name::String; - tags::MLFlowUpsertData{Tag}=Tag[], - description::Union{String,Missing}=missing)::RegisteredModel + tags::MLFlowUpsertData{Tag}=Tag[], description::Union{String,Missing}=missing, + deployment_job_id::Union{String,Missing}=missing)::RegisteredModel result = mlfpost(instance, "registered-models/create"; name=name, - tags=parse(Tag, tags), description=description) + tags=parse(Tag, tags), description=description, + deployment_job_id=deployment_job_id) return result["registered_model"] |> RegisteredModel end @@ -199,13 +200,13 @@ end - `instance:` [`MLFlow`](@ref) configuration. - `name:` [`RegisteredModel`](@ref) name. - `username:` [`User`](@ref) username. -- `permission:` [`Permission`](@ref) to grant. +- `permission:` Permission to grant. # Returns An instance of type [`RegisteredModelPermission`](@ref). """ function createregisteredmodelpermission(instance::MLFlow, name::String, username::String, - permission::Permission)::RegisteredModelPermission + permission::Permission.PermissionEnum)::RegisteredModelPermission result = mlfpost(instance, "registered-models/permissions/create"; name=name, username=username, permission=permission) return result["registered_model_permission"] |> RegisteredModelPermission @@ -237,13 +238,13 @@ end - `instance:` [`MLFlow`](@ref) configuration. - `name:` [`RegisteredModel`](@ref) name. - `username:` [`User`](@ref) username. -- `permission:` New [`Permission`](@ref) to grant. +- `permission:` New permission to grant. # Returns `true` if successful. Otherwise, raises exception. """ function updateregisteredmodelpermission(instance::MLFlow, name::String, username::String, - permission::Permission)::Bool + permission::Permission.PermissionEnum)::Bool mlfpatch(instance, "registered-models/permissions/update"; name=name, username=username, permission=permission) return true diff --git a/src/services/run.jl b/src/services/run.jl index c2d5b5c..c66c440 100644 --- a/src/services/run.jl +++ b/src/services/run.jl @@ -170,8 +170,8 @@ Search for runs that satisfy expressions. Search expressions can use [`Metric`]( - The next page token if there are more results. """ function searchruns(instance::MLFlow; experiment_ids::Array{String}=String[], - filter::String="", run_view_type::ViewType=ACTIVE_ONLY, max_results::Int=1000, - order_by::Array{String}=String[], + filter::String="", run_view_type::ViewType.ViewTypeEnum=ViewType.ACTIVE_ONLY, + max_results::Int=1000, order_by::Array{String}=String[], page_token::String="")::Tuple{Array{Run},Union{String,Nothing}} parameters = (; experiment_ids, filter, :run_view_type => run_view_type |> Integer, max_results, page_token) @@ -207,13 +207,14 @@ Update [`Run`](@ref) metadata. - An instance of type [`RunInfo`](@ref) with the updated metadata. """ function updaterun(instance::MLFlow, run_id::String; - status::Union{RunStatus,Missing}=missing, end_time::Union{Int64,Missing}=missing, - run_name::Union{String,Missing})::RunInfo + status::Union{RunStatus.RunStatusEnum,Missing}=missing, + end_time::Union{Int64,Missing}=missing, run_name::Union{String,Missing})::RunInfo result = mlfpost(instance, "runs/update"; run_id=run_id, status=(status |> Integer), end_time=end_time, run_name=run_name) return result["run_info"] |> RunInfo end -updaterun(instance::MLFlow, run::Run; status::Union{RunStatus,Missing}=missing, +updaterun(instance::MLFlow, run::Run; + status::Union{RunStatus.RunStatusEnum,Missing}=missing, end_time::Union{Int64,Missing}=missing, run_name::Union{String,Missing})::RunInfo = updaterun(instance, run.info.run_id; status=status, end_time=end_time, run_name=run_name) diff --git a/src/types/enums.jl b/src/types/enums.jl index 4edfb6e..24f24d1 100644 --- a/src/types/enums.jl +++ b/src/types/enums.jl @@ -1,71 +1,114 @@ -""" - ModelVersionStatus - -# Members -- `PENDING_REGISTRATION`: Request to register a new model version is pending as server - performs background tasks. -- `FAILED_REGISTRATION`: Request to register a new model version has failed. -- `READY`: Model version is ready for use. -""" -@enum ModelVersionStatus begin - PENDING_REGISTRATION = 1 - FAILED_REGISTRATION = 2 - READY = 3 +module ModelVersionStatus + """ + ModelVersionStatus + + # Members + - `PENDING_REGISTRATION`: Request to register a new model version is pending as server + performs background tasks. + - `FAILED_REGISTRATION`: Request to register a new model version has failed. + - `READY`: Model version is ready for use. + """ + @enum ModelVersionStatusEnum begin + PENDING_REGISTRATION = 1 + FAILED_REGISTRATION = 2 + READY = 3 + end + parse(status::String) = Dict(value => key for (key, value) in ModelVersionStatusEnum |> Base.Enums.namemap)[status|>Symbol] |> ModelVersionStatusEnum end -ModelVersionStatus(status::String) = Dict(value => key for (key, value) in ModelVersionStatus |> Base.Enums.namemap)[status|>Symbol] |> ModelVersionStatus - -""" - RunStatus -Status of a run. - -# Members -- `RUNNING`: Run has been initiated. -- `SCHEDULED`: Run is scheduled to run at a later time. -- `FINISHED`: Run has completed. -- `FAILED`: Run execution failed. -- `KILLED`: Run killed by user. -""" -@enum RunStatus begin - RUNNING = 1 - SCHEDULED = 2 - FINISHED = 3 - FAILED = 4 - KILLED = 5 +module RunStatus + """ + RunStatus + + Status of a run. + + # Members + - `RUNNING`: Run has been initiated. + - `SCHEDULED`: Run is scheduled to run at a later time. + - `FINISHED`: Run has completed. + - `FAILED`: Run execution failed. + - `KILLED`: Run killed by user. + """ + @enum RunStatusEnum begin + RUNNING = 1 + SCHEDULED = 2 + FINISHED = 3 + FAILED = 4 + KILLED = 5 + end + parse(status::String) = Dict(value => key for (key, value) in RunStatusEnum |> Base.Enums.namemap)[status|>Symbol] |> RunStatusEnum end -RunStatus(status::String) = Dict(value => key for (key, value) in RunStatus |> Base.Enums.namemap)[status|>Symbol] |> RunStatus - -""" - ViewType -View type for ListExperiments query. - -# Members -- `ACTIVE_ONLY`: Default. Return only active experiments. -- `DELETED_ONLY`: Return only deleted experiments. -- `ALL`: Get all experiments. -""" -@enum ViewType begin - ACTIVE_ONLY = 1 - DELETED_ONLY = 2 - ALL = 3 +module ViewType + """ + ViewType + + View type for ListExperiments query. + + # Members + - `ACTIVE_ONLY`: Default. Return only active experiments. + - `DELETED_ONLY`: Return only deleted experiments. + - `ALL`: Get all experiments. + """ + @enum ViewTypeEnum begin + ACTIVE_ONLY = 1 + DELETED_ONLY = 2 + ALL = 3 + end end -""" - Permission +module Permission + """ + Permission + + Permission of a user to an experiment or a registered model. + + # Members + - `READ`: Can read. + - `EDIT`: Can read and update. + - `MANAGE`: Can read, update, delete and manage. + - `NO_PERMISSIONS`: No permissions. + """ + @enum PermissionEnum begin + READ = 1 + EDIT = 2 + MANAGE = 3 + NO_PERMISSIONS = 4 + end + parse(permission::String) = Dict(value => key for (key, value) in PermissionEnum |> Base.Enums.namemap)[permission|>Symbol] |> PermissionEnum +end -Permission of a user to an experiment or a registered model. +module DeploymentJobRunState + @enum DeploymentJobRunStateEnum begin + DEPLOYMENT_JOB_RUN_STATE_UNSPECIFIED = 1 + NO_VALID_DEPLOYMENT_JOB_FOUND = 2 + RUNNING = 3 + SUCCEEDED = 4 + FAILED = 5 + PENDING = 6 + APPROVAL = 7 + end + parse(state::String) = Dict(value => key for (key, value) in DeploymentJobRunStateEnum |> Base.Enums.namemap)[state|>Symbol] |> DeploymentJobRunStateEnum +end -# Members -- `READ`: Can read. -- `EDIT`: Can read and update. -- `MANAGE`: Can read, update, delete and manage. -- `NO_PERMISSIONS`: No permissions. -""" -@enum Permission begin - READ = 1 - EDIT = 2 - MANAGE = 3 - NO_PERMISSIONS = 4 +module State + """ + State + + # Members + - `DEPLOYMENT_JOB_CONNECTION_STATE_UNSPECIFIED` + - `NOT_SET_UP`: Default state. + - `CONNECTED`: Connected job: job exists, owner has ACLs, and required job parameters are + present. + - `NOT_FOUND`: Job was deleted or owner had job ACLs removed. + - `REQUIRED_PARAMETERS_CHANGED`: Required job parameters were changed. + """ + @enum StateEnum begin + DEPLOYMENT_JOB_CONNECTION_STATE_UNSPECIFIED = 1 + NOT_SET_UP = 2 + CONNECTED = 3 + NOT_FOUND = 4 + REQUIRED_PARAMETERS_CHANGED = 5 + end + parse(state::String) = Dict(value => key for (key, value) in StateEnum |> Base.Enums.namemap)[state|>Symbol] |> StateEnum end -Permission(permission::String) = Dict(value => key for (key, value) in Permission |> Base.Enums.namemap)[permission|>Symbol] |> Permission diff --git a/src/types/experiment.jl b/src/types/experiment.jl index 26ddcf4..a4a4e5e 100644 --- a/src/types/experiment.jl +++ b/src/types/experiment.jl @@ -31,13 +31,13 @@ Base.show(io::IO, t::Experiment) = show(io, ShowCase(t, new_lines=true)) # Fields - `experiment_id::String`: [`Experiment`](@ref) id. - `user_id::String`: [`User`](@ref) id. -- `permission::Permission`: [`Permission`](@ref) granted. +- `permission::Permission.PermissionEnum`: Permission granted. """ struct ExperimentPermission experiment_id::String user_id::String - permission::Permission + permission::Permission.PermissionEnum end ExperimentPermission(data::Dict{String,Any}) = ExperimentPermission(data["experiment_id"], - data["user_id"] |> string, Permission(data["permission"])) + data["user_id"] |> string, Permission.parse(data["permission"])) Base.show(io::IO, t::ExperimentPermission) = show(io, ShowCase(t, new_lines=true)) diff --git a/src/types/model_version.jl b/src/types/model.jl similarity index 50% rename from src/types/model_version.jl rename to src/types/model.jl index a0542c7..55da4bd 100644 --- a/src/types/model_version.jl +++ b/src/types/model.jl @@ -1,3 +1,62 @@ +""" + ModelInput + +Represents a logged model or [`RegisteredModel`](@ref) version input to a +[`Run`](@ref). + +# Fields +- `model_id::String`: The unique identifier of the model. +""" +struct ModelInput + model_id::String +end + +""" + ModelMetric + +[`Metric`](@ref) associated with a model, represented as a key-value pair. + +# Fields +- `key::String`: Key identifying this metric. +- `value::Float64`: Value associated with this metric. +- `timestamp::Int64`: The timestamp at which this metric was recorded. +- `step::Union{Int64, Nothing}`: Step at which to log the metric. +""" +struct ModelMetric + key::String + value::Float64 + timestamp::Int64 + step::Union{Int64,Nothing} +end + +""" + ModelOutput + +Represents a logged model output of a [`Run`](@ref). + +# Fields +- `model_id::String`: The unique identifier of the model. +- `step::Int64`: Step at which the model was produced. +""" +struct ModelOutput + model_id::String + step::Int64 +end + +""" + ModelParam + +Param for a model version. + +# Fields +- `name::String`: Name of the param. +- `value::String`: Value of the param associated with the name +""" +struct ModelParam + name::String + value::String +end + """ ModelVersion @@ -14,13 +73,19 @@ creating model_version. - `run_id::String`: MLflow run ID used when creating model_version, if source was generated by an experiment run stored in MLflow tracking server. -- `status::ModelVersionStatus`: Current status of model_version. +- `status::ModelVersionStatusEnum`: Current status of model_version. - `status_message::String`: Details on current status, if it is pending or failed. - `tags::Array{Tag}`: Additional metadata key-value pairs. - `run_link::String`: Direct link to the run that generated this version. This field is set at model version creation time only for model versions whose source run is from a tracking server that is different from the registry server. - `aliases::Array{String}`: Aliases pointing to this model_version. +- `model_id::String`: Optional `model_id` for [`ModelVersion`](@ref) that is used to link + the [`RegisteredModel`](@ref) to the source logged model. +- `model_params::Array{ModelParam}`: Optional parameters for the model. +- `model_metrics::Array{ModelMetric}`: Optional metrics for the model. +- `deployment_job_state::ModelVersionDeploymentJobState`: Deployment job state for this + [`ModelVersion`](@ref). """ struct ModelVersion name::String @@ -32,7 +97,7 @@ struct ModelVersion description::String source::String run_id::String - status::ModelVersionStatus + status::ModelVersionStatus.ModelVersionStatusEnum status_message::Union{String,Nothing} tags::Array{Tag} run_link::String @@ -41,7 +106,15 @@ end ModelVersion(data::Dict{String,Any}) = ModelVersion(data["name"], data["version"], data["creation_timestamp"], data["last_updated_timestamp"], get(data, "user_id", nothing), data["current_stage"], data["description"], - data["source"], data["run_id"], ModelVersionStatus(data["status"]), + data["source"], data["run_id"], data["status"] |> ModelVersionStatus.parse, get(data, "status_message", nothing), [Tag(tag) for tag in get(data, "tags", [])], data["run_link"], get(data, "aliases", [])) Base.show(io::IO, t::ModelVersion) = show(io, ShowCase(t, new_lines=true)) + +struct ModelVersionDeploymentJobState + job_id::String + run_id::String + job_state::State.StateEnum + run_state::DeploymentJobRunState.DeploymentJobRunStateEnum + current_task_name::String +end diff --git a/src/types/registered_model.jl b/src/types/registered_model.jl index cb20ef8..a046756 100644 --- a/src/types/registered_model.jl +++ b/src/types/registered_model.jl @@ -30,6 +30,8 @@ Base.show(io::IO, t::RegisteredModelAlias) = show(io, ShowCase(t, new_lines=true - `tags::Array{Tag}`: Additional metadata key-value pairs. - `aliases::Array{RegisteredModelAlias}`: Aliases pointing to model versions associated with this RegisteredModel. +- `deployment_job_id::String`: Deployment job id for this model. +- `deployment_job_state::State`: Deployment job state for this model. """ struct RegisteredModel name::String @@ -40,13 +42,17 @@ struct RegisteredModel latest_versions::Array{ModelVersion} tags::Array{Tag} aliases::Array{RegisteredModelAlias} + deployment_job_id::Union{String,Nothing} + deployment_job_state::Union{State.StateEnum,Nothing} end RegisteredModel(data::Dict{String,Any}) = RegisteredModel(data["name"], data["creation_timestamp"], data["last_updated_timestamp"], get(data, "user_id", nothing), get(data, "description", nothing), [ModelVersion(version) for version in get(data, "latest_versions", [])], [Tag(tag) for tag in get(data, "tags", [])], - [RegisteredModelAlias(alias) for alias in get(data, "aliases", [])]) + [RegisteredModelAlias(alias) for alias in get(data, "aliases", [])], + get(data, "deployment_job_id", nothing), + haskey(data, "deployment_job_state") ? data["deployment_job_state"] |> State.parse : nothing) Base.show(io::IO, t::RegisteredModel) = show(io, ShowCase(t, new_lines=true)) """ @@ -55,13 +61,13 @@ Base.show(io::IO, t::RegisteredModel) = show(io, ShowCase(t, new_lines=true)) # Fields - `name::String`: [`RegisteredModel`](@ref) name. - `user_id::String`: [`User`](@ref) id. -- `permission::Permission`: [`Permission`](@ref) granted. +- `permission::Permission.PermissionEnum`: Permission granted. """ struct RegisteredModelPermission name::String user_id::String - permission::Permission + permission::Permission.PermissionEnum end RegisteredModelPermission(data::Dict{String,Any}) = RegisteredModelPermission(data["name"], - data["user_id"] |> string, Permission(data["permission"])) + data["user_id"] |> string, Permission.parse(data["permission"])) Base.show(io::IO, t::RegisteredModelPermission) = show(io, ShowCase(t, new_lines=true)) diff --git a/src/types/run.jl b/src/types/run.jl index 92ff484..38143a6 100644 --- a/src/types/run.jl +++ b/src/types/run.jl @@ -58,14 +58,14 @@ struct RunInfo run_id::String run_name::String experiment_id::String - status::RunStatus + status::RunStatus.RunStatusEnum start_time::Int64 end_time::Union{Int64,Nothing} artifact_uri::String lifecycle_stage::String end RunInfo(data::Dict{String,Any}) = RunInfo(data["run_id"], data["run_name"], - data["experiment_id"], RunStatus(data["status"]), data["start_time"], + data["experiment_id"], RunStatus.parse(data["status"]), data["start_time"], get(data, "end_time", nothing), data["artifact_uri"], data["lifecycle_stage"]) Base.show(io::IO, t::RunInfo) = show(io, ShowCase(t, new_lines=true)) @@ -100,11 +100,28 @@ Run inputs. """ struct RunInputs dataset_inputs::Array{DatasetInput} + model_inputs::Array{ModelInput} end RunInputs(data::Dict{String,Any}) = RunInputs( - [DatasetInput(dataset_input) for dataset_input in get(data, "dataset_inputs", [])]) + [DatasetInput(dataset_input) for dataset_input in get(data, "dataset_inputs", [])], + [ModelInput(model_input) for model_input in get(data, "model_inputs", [])]) Base.show(io::IO, t::RunInputs) = show(io, ShowCase(t, new_lines=true)) +""" + RunOutputs + +Outputs of a [`Run`](@ref). + +# Fields +- `model_outputs::Array{ModelOutput}`: Model outputs of the [`Run`](@ref). +""" +struct RunOutputs + model_outputs::Array{ModelOutput} +end +RunOutputs(data::Dict{String,Any}) = RunOutputs( + [ModelOutput(model_output) for model_output in get(data, "model_outputs", [])]) +Base.show(io::IO, t::RunOutputs) = show(io, ShowCase(t, new_lines=true)) + """ Run @@ -114,12 +131,14 @@ A single run. - `info::RunInfo`: Metadata of the run. - `data::RunData`: Run data (metrics, params, and tags). - `inputs::RunInputs`: Run inputs. +- `outputs::RunOutputs`: Run outputs. """ struct Run info::RunInfo data::RunData inputs::RunInputs + outputs::RunOutputs end Run(data::Dict{String,Any}) = Run(RunInfo(data["info"]), RunData(data["data"]), - RunInputs(data["inputs"])) + RunInputs(data["inputs"]), RunOutputs(data["outputs"])) Base.show(io::IO, t::Run) = show(io, ShowCase(t, new_lines=true)) diff --git a/test/base.jl b/test/base.jl index 8206d5c..6bf56e8 100644 --- a/test/base.jl +++ b/test/base.jl @@ -17,7 +17,7 @@ end # skips test if mlflow is not available on default location, ENV["MLFLOW_TRACKING_URI"] macro ensuremlf() e = quote - encoded_credentials = Base64.base64encode("admin:password") + encoded_credentials = Base64.base64encode("admin:password1234") mlf = MLFlow(headers=Dict("Authorization" => "Basic $(encoded_credentials)")) mlflow_server_is_running(mlf) || return nothing end diff --git a/test/services/experiment.jl b/test/services/experiment.jl index e713e92..264bb47 100644 --- a/test/services/experiment.jl +++ b/test/services/experiment.jl @@ -219,10 +219,10 @@ end @ensuremlf experiment_id = createexperiment(mlf, UUIDs.uuid4() |> string) - permission = Permission("READ") + permission = Permission.parse("READ") @testset "with string experiment id" begin - user = createuser(mlf, "missy", "gala") + user = createuser(mlf, "missy", "gala12345678") experiment_permission = createexperimentpermission(mlf, experiment_id, user.username, permission) @@ -235,7 +235,7 @@ end end @testset "with integer experiment id" begin - user = createuser(mlf, "missy", "gala") + user = createuser(mlf, "missy", "gala12345678") experiment_permission = createexperimentpermission(mlf, parse(Int, experiment_id), user.username, permission) @@ -249,7 +249,7 @@ end @testset "with Experiment" begin experiment = getexperiment(mlf, experiment_id) - user = createuser(mlf, "missy", "gala") + user = createuser(mlf, "missy", "gala12345678") experiment_permission = createexperimentpermission(mlf, experiment, user.username, permission) @@ -268,8 +268,8 @@ end @ensuremlf experiment_id = createexperiment(mlf, UUIDs.uuid4() |> string) - permission = Permission("READ") - user = createuser(mlf, "missy", "gala") + permission = Permission.parse("READ") + user = createuser(mlf, "missy", "gala12345678") @testset "with string experiment id" begin createexperimentpermission(mlf, experiment_id, user.username, permission) @@ -313,34 +313,34 @@ end @ensuremlf experiment_id = createexperiment(mlf, UUIDs.uuid4() |> string) - permission = Permission("READ") - user = createuser(mlf, "missy", "gala") + permission = Permission.parse("READ") + user = createuser(mlf, "missy", "gala12345678") @testset "with string experiment id" begin createexperimentpermission(mlf, experiment_id, user.username, permission) - updateexperimentpermission(mlf, experiment_id, user.username, Permission("EDIT")) + updateexperimentpermission(mlf, experiment_id, user.username, Permission.parse("EDIT")) experiment_permission = getexperimentpermission(mlf, experiment_id, user.username) - @test experiment_permission.permission == Permission("EDIT") + @test experiment_permission.permission == Permission.parse("EDIT") deleteexperimentpermission(mlf, experiment_id, user.username) end @testset "with integer experiment id" begin createexperimentpermission(mlf, parse(Int, experiment_id), user.username, permission) - updateexperimentpermission(mlf, parse(Int, experiment_id), user.username, Permission("EDIT")) + updateexperimentpermission(mlf, parse(Int, experiment_id), user.username, Permission.parse("EDIT")) experiment_permission = getexperimentpermission(mlf, parse(Int, experiment_id), user.username) - @test experiment_permission.permission == Permission("EDIT") + @test experiment_permission.permission == Permission.parse("EDIT") deleteexperimentpermission(mlf, experiment_id, user.username) end @testset "with Experiment" begin experiment = getexperiment(mlf, experiment_id) createexperimentpermission(mlf, experiment, user.username, permission) - updateexperimentpermission(mlf, experiment, user.username, Permission("EDIT")) + updateexperimentpermission(mlf, experiment, user.username, Permission.parse("EDIT")) experiment_permission = getexperimentpermission(mlf, experiment, user.username) - @test experiment_permission.permission == Permission("EDIT") + @test experiment_permission.permission == Permission.parse("EDIT") deleteexperimentpermission(mlf, experiment_id, user.username) end @@ -352,8 +352,8 @@ end @ensuremlf experiment_id = createexperiment(mlf, UUIDs.uuid4() |> string) - permission = Permission("READ") - user = createuser(mlf, "missy", "gala") + permission = Permission.parse("READ") + user = createuser(mlf, "missy", "gala12345678") @testset "with string experiment id" begin createexperimentpermission(mlf, experiment_id, user.username, permission) diff --git a/test/services/logger.jl b/test/services/logger.jl index 32bad21..2d77ea6 100644 --- a/test/services/logger.jl +++ b/test/services/logger.jl @@ -298,3 +298,40 @@ end deleteexperiment(mlf, experiment_id) end + +@testset verbose = true "log model" begin + @ensuremlf + + experiment_id = createexperiment(mlf, UUIDs.uuid4() |> string) + mlmodel_json_string = """ + { + "time_created": "2018-05-25T17:28:53.35", + "flavors": { + "sklearn": { + "sklearn_version": "0.19.1", + "pickled_model": "model.pkl" + }, + "python_function": { + "loader_module": "mlflow.sklearn" + } + }, + "utc_time_created": "2025-08-16T16:50:00.00Z", + "artifact_path": "mlflowclientjl-test", + "run_id": "" + } + """ + + @testset "with run id as string" begin + run = createrun(mlf, experiment_id) + @assert logmodel(mlf, run.info.run_id, replace(mlmodel_json_string, "" => run.info.run_id)) + deleterun(mlf, run) + end + + @testset "with run" begin + run = createrun(mlf, experiment_id) + @assert logmodel(mlf, run, replace(mlmodel_json_string, "" => run.info.run_id)) + deleterun(mlf, run) + end + + deleteexperiment(mlf, experiment_id) +end diff --git a/test/services/registered_model.jl b/test/services/registered_model.jl index 3140099..baf7503 100644 --- a/test/services/registered_model.jl +++ b/test/services/registered_model.jl @@ -181,13 +181,13 @@ end @ensuremlf registered_model = createregisteredmodel(mlf, "missy"; description="gala") - user = createuser(mlf, "missy", "gala") - permission = createregisteredmodelpermission(mlf, registered_model.name, user.username, Permission("READ")) + user = createuser(mlf, "missy", "gala12345678") + permission = createregisteredmodelpermission(mlf, registered_model.name, user.username, "READ" |> Permission.parse) @test permission isa RegisteredModelPermission @test permission.name == registered_model.name @test permission.user_id == user.id - @test permission.permission == Permission("READ") + @test permission.permission == ("READ" |> Permission.parse) deleteregisteredmodelpermission(mlf, registered_model.name, user.username) deleteuser(mlf, user.username) @@ -198,14 +198,14 @@ end @ensuremlf registered_model = createregisteredmodel(mlf, "missy"; description="gala") - user = createuser(mlf, "missy", "gala") - permission = createregisteredmodelpermission(mlf, registered_model.name, user.username, Permission("READ")) + user = createuser(mlf, "missy", "gala12345678") + permission = createregisteredmodelpermission(mlf, registered_model.name, user.username, "READ" |> Permission.parse) retrieved_permission = getregisteredmodelpermission(mlf, registered_model.name, user.username) @test retrieved_permission isa RegisteredModelPermission @test retrieved_permission.name == registered_model.name @test retrieved_permission.user_id == user.id - @test retrieved_permission.permission == Permission("READ") + @test retrieved_permission.permission == ("READ" |> Permission.parse) deleteregisteredmodelpermission(mlf, registered_model.name, user.username) deleteuser(mlf, user.username) @@ -216,15 +216,15 @@ end @ensuremlf registered_model = createregisteredmodel(mlf, "missy"; description="gala") - user = createuser(mlf, "missy", "gala") - permission = createregisteredmodelpermission(mlf, registered_model.name, user.username, Permission("READ")) - updateregisteredmodelpermission(mlf, registered_model.name, user.username, Permission("MANAGE")) + user = createuser(mlf, "missy", "gala12345678") + permission = createregisteredmodelpermission(mlf, registered_model.name, user.username, "READ" |> Permission.parse) + updateregisteredmodelpermission(mlf, registered_model.name, user.username, "MANAGE" |> Permission.parse) retrieved_permission = getregisteredmodelpermission(mlf, registered_model.name, user.username) @test retrieved_permission isa RegisteredModelPermission @test retrieved_permission.name == registered_model.name @test retrieved_permission.user_id == user.id - @test retrieved_permission.permission == Permission("MANAGE") + @test retrieved_permission.permission == ("MANAGE" |> Permission.parse) deleteregisteredmodelpermission(mlf, registered_model.name, user.username) deleteuser(mlf, user.username) @@ -235,8 +235,8 @@ end @ensuremlf registered_model = createregisteredmodel(mlf, "missy"; description="gala") - user = createuser(mlf, "missy", "gala") - permission = createregisteredmodelpermission(mlf, registered_model.name, user.username, Permission("READ")) + user = createuser(mlf, "missy", "gala12345678") + permission = createregisteredmodelpermission(mlf, registered_model.name, user.username, "READ" |> Permission.parse) deleteregisteredmodelpermission(mlf, registered_model.name, user.username) @test_throws ErrorException getregisteredmodelpermission(mlf, registered_model.name, user.username) diff --git a/test/services/run.jl b/test/services/run.jl index fb7af1b..9155c40 100644 --- a/test/services/run.jl +++ b/test/services/run.jl @@ -192,7 +192,7 @@ end run = createrun(mlf, experiment_id) @testset "update with string id" begin - status = MLFlowClient.FINISHED + status = MLFlowClient.RunStatus.FINISHED end_time = 123 run_name = "missy" @@ -204,7 +204,7 @@ end end @testset "update with Run" begin - status = MLFlowClient.FAILED + status = MLFlowClient.RunStatus.FAILED end_time = 456 run_name = "gala" diff --git a/test/services/user.jl b/test/services/user.jl index 958a155..6512037 100644 --- a/test/services/user.jl +++ b/test/services/user.jl @@ -1,7 +1,7 @@ @testset verbose = true "create user" begin @ensuremlf - user = createuser(mlf, "missy", "gala") + user = createuser(mlf, "missy", "gala12345678") @test user isa User @test user.username == "missy" @@ -13,7 +13,7 @@ end @testset verbose = true "get user" begin @ensuremlf - user = createuser(mlf, "missy", "gala") + user = createuser(mlf, "missy", "gala12345678") retrieved_user = getuser(mlf, "missy") @@ -30,11 +30,11 @@ end getmlfinstance(encoded_credentials::String) = MLFlow(headers=Dict("Authorization" => "Basic $(encoded_credentials)")) - user = createuser(mlf, "missy", "gala") - encoded_credentials = Base64.base64encode("$(user.username):gala") + user = createuser(mlf, "missy", "gala12345678") + encoded_credentials = Base64.base64encode("$(user.username):gala12345678") - updateuserpassword(getmlfinstance(encoded_credentials), "missy", "ana") - encoded_credentials = Base64.base64encode("$(user.username):ana") + updateuserpassword(getmlfinstance(encoded_credentials), "missy", "ana12345678") + encoded_credentials = Base64.base64encode("$(user.username):ana12345678") @test begin try @@ -50,7 +50,7 @@ end @testset verbose = true "update user admin" begin @ensuremlf - user = createuser(mlf, "missy", "gala") + user = createuser(mlf, "missy", "gala12345678") updateuseradmin(mlf, "missy", true) retrieved_user = getuser(mlf, "missy") @@ -62,7 +62,7 @@ end @testset verbose = true "delete user" begin @ensuremlf - user = createuser(mlf, "missy", "gala") + user = createuser(mlf, "missy", "gala12345678") deleteuser(mlf, "missy") @test_throws ErrorException getuser(mlf, "missy")