Skip to content

Commit dd904fa

Browse files
committed
Finishing with experiments endpoints (searchexperiments not complete)
1 parent 6382569 commit dd904fa

File tree

9 files changed

+477
-97
lines changed

9 files changed

+477
-97
lines changed

src/MLFlowClient.jl

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
"""
2-
MLFlowClient
3-
4-
[MLFlowClient](https://github.com/JuliaAI.jl) is a [Julia](https://julialang.org/) package for working with [MLFlow](https://mlflow.org/) using the REST [API v2.0](https://www.mlflow.org/docs/latest/rest-api.html).
5-
6-
`MLFlowClient` allows you to create and manage `MLFlow` experiments, runs, and log metrics and artifacts. If you are not familiar with `MLFlow` and its concepts, please refer to [MLFlow documentation](https://mlflow.org/docs/latest/index.html).
7-
8-
# Limitations
9-
10-
- no authentication support.
11-
- when storing artifacts, the assumption is that MLFlow and this library run on the same server. Artifacts are stored using plain filesystem operations. Therefore, `/mlruns` or the specified `artifact_location` must be accessible to both the MLFlow server (read), and this library (write).
12-
"""
131
module MLFlowClient
142

153
using Dates
@@ -21,18 +9,55 @@ using ShowCases
219
using FilePathsBase: AbstractPath
2210

2311
include("types/tag.jl")
12+
export Tag
13+
2414
include("types/enums.jl")
15+
export
16+
ViewType,
17+
RunStatus,
18+
ModelVersionStatus
19+
2520
include("types/dataset.jl")
21+
export
22+
Dataset,
23+
DatasetInput
24+
2625
include("types/artifact.jl")
26+
export FileInfo
27+
2728
include("types/model_version.jl")
29+
export ModelVersion
30+
2831
include("types/registered_model.jl")
32+
export
33+
RegisteredModel,
34+
RegisteredModelAlias
35+
2936
include("types/experiment.jl")
37+
export Experiment
38+
3039
include("types/run.jl")
40+
export
41+
Run,
42+
Param,
43+
Metric,
44+
RunData,
45+
RunInfo,
46+
RunInputs
47+
3148
include("types/mlflow.jl")
49+
export MLFlow
3250

3351
include("utils.jl")
3452
include("api.jl")
3553

3654
include("services/experiment.jl")
37-
55+
export
56+
getexperiment,
57+
createexperiment,
58+
deleteexperiment,
59+
updateexperiment,
60+
restoreexperiment,
61+
searchexperiments,
62+
getexperimentbyname
3863
end

src/api.jl

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,38 @@
1+
"""
2+
uri(mlf::MLFlow, endpoint::String; parameters=missing)
3+
4+
Retrieves an URI based on `mlf`, `endpoint`, and, optionally, `parameters`.
5+
6+
# Examples
7+
```@example
8+
MLFlowClient.uri(mlf, "experiments/get", Dict(:experiment_id=>10))
9+
```
10+
"""
11+
uri(mlf::MLFlow, endpoint::String;
12+
parameters::Dict{Symbol, <:Any}=Dict{Symbol, IntOrString}()) =
13+
URI("$(mlf.apiroot)/$(mlf.apiversion)/mlflow/$(endpoint)";
14+
query=parameters)
15+
16+
"""
17+
headers(mlf::MLFlow,custom_headers::AbstractDict)
18+
19+
Retrieves HTTP headers based on `mlf` and merges with user-provided `custom_headers`
20+
21+
# Examples
22+
```@example
23+
headers(mlf,Dict("Content-Type"=>"application/json"))
24+
```
25+
"""
26+
headers(mlf::MLFlow, custom_headers::AbstractDict) = merge(mlf.headers, custom_headers)
27+
128
"""
229
mlfget(mlf, endpoint; kwargs...)
330
431
Performs a HTTP GET to a specified endpoint. kwargs are turned into GET params.
532
"""
633
function mlfget(mlf, endpoint; kwargs...)
7-
apiuri = uri(mlf, endpoint, kwargs)
8-
apiheaders = headers(mlf, Dict("Content-Type" => "application/json"))
34+
apiuri = uri(mlf, endpoint; parameters=kwargs |> Dict)
35+
apiheaders = headers(mlf, ("Content-Type" => "application/json") |> Dict)
936

1037
try
1138
response = HTTP.get(apiuri, apiheaders)
@@ -21,7 +48,7 @@ end
2148
Performs a HTTP POST to the specified endpoint. kwargs are converted to JSON and become the POST body.
2249
"""
2350
function mlfpost(mlf, endpoint; kwargs...)
24-
apiuri = uri(mlf, endpoint)
51+
apiuri = uri(mlf, endpoint;)
2552
apiheaders = headers(mlf, Dict("Content-Type" => "application/json"))
2653
body = JSON.json(kwargs)
2754

@@ -31,4 +58,4 @@ function mlfpost(mlf, endpoint; kwargs...)
3158
catch e
3259
throw(e)
3360
end
34-
end
61+
end

src/services/experiment.jl

Lines changed: 188 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,50 @@
11
"""
2-
createexperiment(instance::MLFlow; name=missing, artifact_location=missing,
3-
tags=[])
2+
createexperiment(instance::MLFlow; name::String="",
3+
artifact_location::String="",
4+
tags::Union{Dict{<:Any}, Array{<:Any}}=[])
45
56
Create an experiment with a name. Returns the newly created experiment.
67
Validates that another experiment with the same name does not already exist and
78
fails if another experiment with the same name already exists.
89
910
# Arguments
1011
- `instance`: [`MLFlow`](@ref) configuration.
11-
- `name::String`: Experiment name. This field is required.
12-
- `artifact_location::String`: Location where all artifacts for the experiment
12+
- `name`: Experiment name. This field is required.
13+
- `artifact_location`: Location where all artifacts for the experiment
1314
are stored. If not provided, the remote server will select an appropriate
1415
default.
1516
- `tags`: A collection of tags to set on the experiment.
1617
1718
# Returns
18-
An object of type [`Experiment`](@ref).
19+
The ID of the newly created experiment.
1920
"""
20-
function createexperiment(instance::MLFlow; name::String=missing,
21-
artifact_location::String=missing, tags::Array{Dict{Any, Any}}=[])
22-
if ismissing(name)
23-
name = string(UUIDs.uuid4())
21+
function createexperiment(instance::MLFlow; name::String="",
22+
artifact_location::String="",
23+
tags::Union{Dict{<:Any}, Array{<:Any}}=[])::String
24+
if name |> isempty
25+
name = UUIDs.uuid4() |> string
2426
end
2527

28+
tags = tags |> parsetags
29+
2630
try
2731
result = mlfpost(instance, "experiments/create"; name=name,
2832
artifact_location=artifact_location, tags=tags)
29-
return getexperiment(instance, result["experiment_id"])
33+
return result["experiment_id"]
3034
catch e
3135
if isa(e, HTTP.ExceptionRequest.StatusError) && e.status == 400
32-
error_code = JSON.parse(String(e.response.body))["error_code"]
36+
error_code = (e.response.body |> String |> JSON.parse)["error_code"]
3337
if error_code == MLFLOW_ERROR_CODES.RESOURCE_ALREADY_EXISTS
3438
error("Experiment with name \"$name\" already exists")
3539
end
3640
end
3741
throw(e)
3842
end
3943
end
40-
createexperiment(instance::MLFlow; name::String=missing,
41-
artifact_location::String=missing, tags::Array{Pair{Any, Any}}=[]) =
42-
createexperiment(instance, name=name, artifact_location=artifact_location,
43-
tags=tags |> transform_pair_array_to_dict_array)
44-
createexperiment(instance::MLFlow; name::String=missing,
45-
artifact_location::String=missing, tags::Dict{Any, Any}=[]) =
46-
createexperiment(instance, name=name, artifact_location=artifact_location,
47-
tags=tags |> transform_dict_to_dict_array)
48-
createexperiment(instance::MLFlow; name::String=missing,
49-
artifact_location::String=missing, tags::Array{Tag}=[]) =
50-
createexperiment(instance, name=name, artifact_location=artifact_location,
51-
tags=tags |> transform_tag_array_to_dict_array)
5244

5345
"""
5446
getexperiment(instance::MLFlow, experiment_id::String)
47+
getexperiment(instance::MLFlow, experiment_id::Integer)
5548
5649
Get metadata for an experiment. This method works on deleted experiments.
5750
@@ -66,7 +59,7 @@ function getexperiment(instance::MLFlow, experiment_id::String)
6659
try
6760
arguments = (:experiment_id => experiment_id,)
6861
result = mlfget(instance, "experiments/get"; arguments...)
69-
return Experiment(result["experiment"])
62+
return result["experiment"] |> Experiment
7063
catch e
7164
if isa(e, HTTP.ExceptionRequest.StatusError) && e.status == 404
7265
return missing
@@ -75,4 +68,174 @@ function getexperiment(instance::MLFlow, experiment_id::String)
7568
end
7669
end
7770
getexperiment(instance::MLFlow, experiment_id::Integer) =
78-
getexperiment(instance, experiment_id)
71+
getexperiment(instance, string(experiment_id))
72+
73+
"""
74+
getexperimentbyname(instance::MLFlow, experiment_name::String)
75+
76+
Get metadata for an experiment.
77+
78+
This endpoint will return deleted experiments, but prefers the active
79+
experiment if an active and deleted experiment share the same name. If multiple
80+
deleted experiments share the same name, the API will return one of them.
81+
82+
# Arguments
83+
- `instance`: [`MLFlow`](@ref) configuration.
84+
- `experiment_name`: Name of the associated experiment.
85+
86+
# Returns
87+
An object of type [`Experiment`](@ref).
88+
"""
89+
function getexperimentbyname(instance::MLFlow, experiment_name::String)
90+
try
91+
arguments = (:experiment_name => experiment_name,)
92+
result = mlfget(instance, "experiments/get-by-name"; arguments...)
93+
return result["experiment"] |> Experiment
94+
catch e
95+
if isa(e, HTTP.ExceptionRequest.StatusError) && e.status == 404
96+
return missing
97+
end
98+
throw(e)
99+
end
100+
end
101+
102+
"""
103+
deleteexperiment(mlf::MLFlow, experiment_id::String)
104+
deleteexperiment(mlf::MLFlow, experiment_id::Integer)
105+
deleteexperiment(mlf::MLFlow, experiment::Experiment)
106+
107+
Mark an experiment and associated metadata, runs, metrics, params, and tags for
108+
deletion. If the experiment uses FileStore, artifacts associated with
109+
experiment are also deleted.
110+
111+
# Arguments
112+
- `mlf`: [`MLFlow`](@ref) configuration.
113+
- `experiment_id`: ID of the associated experiment.
114+
115+
# Returns
116+
117+
`true` if successful. Otherwise, raises exception.
118+
"""
119+
function deleteexperiment(mlf::MLFlow, experiment_id::String)
120+
endpoint = "experiments/delete"
121+
try
122+
mlfpost(mlf, endpoint; experiment_id=experiment_id)
123+
return true
124+
catch e
125+
if isa(e, HTTP.ExceptionRequest.StatusError) && e.status == 404
126+
# experiment already deleted
127+
return true
128+
end
129+
throw(e)
130+
end
131+
end
132+
deleteexperiment(mlf::MLFlow, experiment_id::Integer) =
133+
deleteexperiment(mlf, string(experiment_id))
134+
deleteexperiment(mlf::MLFlow, experiment::Experiment) =
135+
deleteexperiment(mlf, experiment.experiment_id)
136+
137+
"""
138+
restoreexperiment(mlf::MLFlow, experiment_id::String)
139+
restoreexperiment(mlf::MLFlow, experiment_id::Integer)
140+
restoreexperiment(mlf::MLFlow, experiment::Experiment)
141+
142+
Restore an experiment marked for deletion. This also restores associated
143+
metadata, runs, metrics, params, and tags. If experiment uses FileStore,
144+
underlying artifacts associated with experiment are also restored.
145+
146+
# Arguments
147+
- `mlf`: [`MLFlow`](@ref) configuration.
148+
- `experiment_id`: ID of the associated experiment.
149+
150+
# Returns
151+
152+
`true` if successful. Otherwise, raises exception.
153+
"""
154+
function restoreexperiment(mlf::MLFlow, experiment_id::String)
155+
endpoint = "experiments/restore"
156+
try
157+
mlfpost(mlf, endpoint; experiment_id=experiment_id)
158+
return true
159+
catch e
160+
if isa(e, HTTP.ExceptionRequest.StatusError) && e.status == 404
161+
error_code = JSON.parse(String(e.response.body))["error_code"]
162+
if error_code == MLFLOW_ERROR_CODES.RESOURCE_DOES_NOT_EXIST
163+
error("Experiment with id \"$experiment_id\" does not exist")
164+
end
165+
end
166+
throw(e)
167+
end
168+
end
169+
restoreexperiment(mlf::MLFlow, experiment_id::Integer) =
170+
deleteexperiment(mlf, string(experiment_id))
171+
restoreexperiment(mlf::MLFlow, experiment::Experiment) =
172+
deleteexperiment(mlf, experiment.experiment_id)
173+
174+
"""
175+
updateexperiment(mlf::MLFlow, experiment_id::String, new_name::String)
176+
updateexperiment(mlf::MLFlow, experiment_id::Integer, new_name::String)
177+
updateexperiment(mlf::MLFlow, experiment::Experiment, new_name::String)
178+
179+
Update experiment metadata.
180+
181+
# Arguments
182+
- `mlf`: [`MLFlow`](@ref) configuration.
183+
- `experiment_id`: ID of the associated experiment.
184+
- `new_name`: If provided, the experiment’s name is changed to the new name.
185+
The new name must be unique.
186+
187+
# Returns
188+
`true` if successful. Otherwise, raises exception.
189+
"""
190+
function updateexperiment(mlf::MLFlow, experiment_id::String, new_name::String)
191+
endpoint = "experiments/update"
192+
try
193+
mlfpost(mlf, endpoint; experiment_id=experiment_id, new_name=new_name)
194+
return true
195+
catch e
196+
throw(e)
197+
end
198+
end
199+
updateexperiment(mlf::MLFlow, experiment_id::Integer, new_name::String) =
200+
updateexperiment(mlf, string(experiment_id), new_name)
201+
updateexperiment(mlf::MLFlow, experiment::Experiment, new_name::String) =
202+
updateexperiment(mlf, experiment.experiment_id, new_name::String)
203+
204+
"""
205+
searchexperiments(mlf::MLFlow; max_results::Integer=20000,
206+
page_token::String="", filter::String="", order_by::Array{String}=[],
207+
view_type::ViewType=ACTIVE_ONLY)
208+
209+
# Arguments
210+
- `mlf`: [`MLFlow`](@ref) configuration.
211+
- `max_results`: Maximum number of experiments desired.
212+
- `page_token`: Token indicating the page of experiments to fetch.
213+
- `filter`: A filter expression over experiment attributes and tags that allows
214+
returning a subset of experiments. See [MLFlow documentation](https://mlflow.org/docs/latest/rest-api.html#search-experiments).
215+
- `order_by`: List of columns for ordering search results, which can include
216+
experiment name and id with an optional “DESC” or “ASC” annotation, where “ASC”
217+
is the default.
218+
- `view_type`: Qualifier for type of experiments to be returned. If
219+
unspecified, return only active experiments.
220+
221+
# Returns
222+
- vector of [`MLFlowExperiment`](@ref) experiments that were found in the MLFlow instance
223+
"""
224+
function searchexperiments(mlf::MLFlow; max_results::Integer=20000,
225+
page_token::String="", filter::String="", order_by::Array{String}=String[],
226+
view_type::ViewType=ACTIVE_ONLY)
227+
endpoint = "experiments/search"
228+
parameters = (; max_results, page_token, filter,
229+
:view_type => view_type |> Integer)
230+
231+
if order_by |> !isempty
232+
parameters = (; order_by, parameters...)
233+
end
234+
235+
try
236+
result = mlfget(mlf, endpoint; parameters...)
237+
return result["experiments"] |> (x -> [Experiment(y) for y in x])
238+
catch e
239+
throw(e)
240+
end
241+
end

0 commit comments

Comments
 (0)