2
2
"""
3
3
This is a Python script that will copy/promote a Verta Build from one environment
4
4
to another.
5
-
6
5
- The script will use the model version passed in the VERTA_SOURCE_MODEL_VERSION_ID environment variable
7
6
as the model version to promote.
8
7
- The latest self-contained build of the model version will be promoted. The promotion process will terminate if no
9
8
self-contained builds of the model version are found.
10
9
- If you need to create a self-contained build of a model version, use the create_scb.py script.
11
-
12
- Configuration is done via environment variables. All are mandatory except VERTA_DEST_REGISTERED_MODEL:
13
-
10
+ Configuration is done via environment variables. All are mandatory except VERTA_DEST_REGISTERED_MODEL_ID:
14
11
- VERTA_SOURCE_MODEL_VERSION_ID: The ID of the model version to promote
15
12
- VERTA_SOURCE_HOST: The source Verta instance to promote from
16
13
- VERTA_SOURCE_EMAIL: The email address for authentication to the source Verta instance
17
14
- VERTA_SOURCE_DEV_KEY: The dev key associated to the email address on the source Verta instance
18
- - VERTA_SOURCE_WORKSPACE : The workspace associated with the build on the source Verta instance
15
+ - VERTA_SOURCE_WORKSPACE_0 : The workspace associated with the build on the source Verta instance
19
16
- VERTA_DEST_HOST: The destination Verta instance to promote to
20
17
- VERTA_DEST_EMAIL: The email address for authentication to the destination Verta instance
21
18
- VERTA_DEST_DEV_KEY: The dev key associated to the email address on the destination Verta instance
22
- - VERTA_DEST_WORKSPACE: The name of the workspace associated with the build on the destination Verta instance
23
- - VERTA_DEST_REGISTERED_MODEL: [optional] The name of the registered model to promote to. If missing, we'll create a new registered model
24
-
19
+ - VERTA_DEST_WORKSPACE: The workspace associated with the build on the destination Verta instance
20
+ - VERTA_DEST_REGISTERED_MODEL_ID: [optional] The ID of the registered model to promote to. If missing, we'll create a new registered model
25
21
Optional environment variables to configure curl usage:
26
22
VERTA_CURL_OPTS: Options to pass to curl. Defaults to '-O'
27
-
28
23
With these values set, run the script. The script will not attempt to delete any data and will fail if the registered
29
24
model (if an existing one has not been provided) or version already exists in the destination.
30
25
"""
35
30
import os
36
31
import datetime
37
32
38
- env_vars = ['VERTA_SOURCE_MODEL_VERSION_ID' , 'VERTA_SOURCE_HOST' , 'VERTA_SOURCE_EMAIL' , 'VERTA_SOURCE_DEV_KEY' ,
39
- 'VERTA_SOURCE_WORKSPACE' , 'VERTA_DEST_HOST' , 'VERTA_DEST_EMAIL' , 'VERTA_DEST_DEV_KEY' ,
33
+ env_vars = ['VERTA_SOURCE_MODEL_VERSION_ID' , 'VERTA_SOURCE_HOST' , 'VERTA_SOURCE_EMAIL' ,
34
+ 'VERTA_SOURCE_DEV_KEY' ,
35
+ 'VERTA_SOURCE_WORKSPACE_0' , 'VERTA_DEST_HOST' , 'VERTA_DEST_EMAIL' ,
36
+ 'VERTA_DEST_DEV_KEY' ,
40
37
'VERTA_DEST_WORKSPACE' ]
41
- opt_env_vars = ['VERTA_DEST_REGISTERED_MODEL' ]
38
+
39
+ opt_env_vars = ['VERTA_DEST_REGISTERED_MODEL_ID' ]
40
+
42
41
params = {}
43
42
43
+ proxies = {
44
+ "http" : None ,
45
+ "https" : None
46
+ }
47
+
48
+ if not os .environ .get ('VERTA_DEST_WORKSPACE' ):
49
+ host = 'https://' + os .environ .get (
50
+ 'VERTA_DEST_HOST' ) + '/api/v1/uac-proxy/workspace/getVisibleWorkspaces'
51
+ headers_dict = {'grpc-metadata-source' : 'PythonClient' ,
52
+ 'grpc-metadata-email' : os .environ .get ('VERTA_DEST_EMAIL' ),
53
+ 'grpc-metadata-developer_key' : os .environ .get ('VERTA_DEST_DEV_KEY' )}
54
+ workspaces_dest = requests .get (host , headers = headers_dict , proxies = proxies )
55
+
56
+ source_workspace_id = os .environ .get ('VERTA_SOURCE_WORKSPACE_0' )
57
+ host = 'https://' + os .environ .get (
58
+ 'VERTA_SOURCE_HOST' ) + '/api/v1/uac-proxy/workspace/getVisibleWorkspaces'
59
+ headers_dict = {'grpc-metadata-source' : 'PythonClient' ,
60
+ 'grpc-metadata-email' : os .environ .get ('VERTA_SOURCE_EMAIL' ),
61
+ 'grpc-metadata-developer_key' : os .environ .get ('VERTA_SOURCE_DEV_KEY' )}
62
+ workspaces_source = requests .get (host , headers = headers_dict , proxies = proxies )
63
+
64
+ for item in workspaces_source .json ()['workspace' ]:
65
+ if 'id' in item .keys () and item ['id' ] == source_workspace_id :
66
+ if 'org_name' in item .keys ():
67
+ source_workspace = item ['org_name' ]
68
+ else :
69
+ source_workspace = item ['username' ]
70
+
71
+ if source_workspace == None :
72
+ print ('Source workspace ID could not be matched' )
73
+
74
+ for item in workspaces_dest .json ()['workspace' ]:
75
+ if 'org_name' in item .keys () and item ['org_name' ] == source_workspace :
76
+ os .environ ['VERTA_DEST_WORKSPACE' ] = item ['org_name' ]
77
+ elif 'username' in item .keys () and item ['username' ] == source_workspace :
78
+ os .environ ['VERTA_DEST_WORKSPACE' ] = item ['username' ]
44
79
45
80
for param_name in env_vars :
46
81
param = os .environ .get (param_name )
57
92
params ['VERTA_CURL_OPTS' ] = curl_opts
58
93
else :
59
94
params ['VERTA_CURL_OPTS' ] = ''
95
+ params ['VERTA_CURL_OPTS' ] += f' -H @curl_headers'
60
96
61
97
config = {
62
98
'source' : {
63
- 'model_version_id' : atoi (params ['VERTA_SOURCE_MODEL_VERSION_ID' ]),
99
+ 'model_version_id' : atoi (params ['VERTA_SOURCE_MODEL_VERSION_ID' ][ 2 : - 2 ] ),
64
100
'host' : params ['VERTA_SOURCE_HOST' ],
65
101
'email' : params ['VERTA_SOURCE_EMAIL' ],
66
102
'devkey' : params ['VERTA_SOURCE_DEV_KEY' ],
67
- 'workspace' : params ['VERTA_SOURCE_WORKSPACE' ]
103
+ 'workspace' : params ['VERTA_SOURCE_WORKSPACE_0' ],
104
+ 'workspace_name' : source_workspace
68
105
},
69
106
'dest' : {
70
107
'host' : params ['VERTA_DEST_HOST' ],
71
108
'email' : params ['VERTA_DEST_EMAIL' ],
72
109
'devkey' : params ['VERTA_DEST_DEV_KEY' ],
73
110
'workspace' : params ['VERTA_DEST_WORKSPACE' ],
74
- 'registered_model_name ' : params ['VERTA_DEST_REGISTERED_MODEL' ] # Will be empty if no destination RM was provided
111
+ 'registered_model_id ' : params ['VERTA_DEST_REGISTERED_MODEL_ID' ]
75
112
}
76
113
}
77
114
@@ -84,7 +121,8 @@ def copy_fields(fields, src, dest):
84
121
85
122
def auth_context (host , email , devkey , workspace ):
86
123
return {'headers' : {'Grpc-metadata-scheme' : 'https' , 'Grpc-metadata-source' : 'PythonClient' ,
87
- 'Grpc-metadata-email' : email , 'Grpc-metadata-developer_key' : devkey }, 'host' : host ,
124
+ 'Grpc-metadata-email' : email , 'Grpc-metadata-developer_key' : devkey },
125
+ 'host' : host ,
88
126
'workspace' : workspace
89
127
}
90
128
@@ -93,7 +131,8 @@ def post(auth, path, body):
93
131
auth ['headers' ]['Content-Type' ] = 'application/json'
94
132
body ['workspaceName' ] = auth ['workspace' ]
95
133
try :
96
- res = requests .post ("https://{}{}" .format (auth ["host" ], path ), headers = auth ['headers' ], json = body )
134
+ res = requests .post ("https://{}{}" .format (auth ["host" ], path ), headers = auth ['headers' ],
135
+ json = body , proxies = proxies )
97
136
res .raise_for_status ()
98
137
except requests .exceptions .RequestException as e :
99
138
if e .response .text :
@@ -104,7 +143,8 @@ def post(auth, path, body):
104
143
105
144
def get (auth , path ):
106
145
try :
107
- res = requests .get ("https://{}{}" .format (auth ["host" ], path ), headers = auth ['headers' ])
146
+ res = requests .get ("https://{}{}" .format (auth ["host" ], path ), headers = auth ['headers' ],
147
+ proxies = proxies )
108
148
res .raise_for_status ()
109
149
except requests .exceptions .RequestException as e :
110
150
if e .response .text :
@@ -115,7 +155,8 @@ def get(auth, path):
115
155
116
156
def put (auth , path , body ):
117
157
try :
118
- res = requests .put ("https://{}{}" .format (auth ["host" ], path ), headers = auth ['headers' ], json = body )
158
+ res = requests .put ("https://{}{}" .format (auth ["host" ], path ), headers = auth ['headers' ],
159
+ json = body , proxies = proxies )
119
160
res .raise_for_status ()
120
161
except requests .exceptions .RequestException as e :
121
162
if e .response .text :
@@ -128,7 +169,8 @@ def put(auth, path, body):
128
169
129
170
def patch (auth , path , body ):
130
171
try :
131
- res = requests .patch ("https://{}{}" .format (auth ["host" ], path ), headers = auth ['headers' ], json = body )
172
+ res = requests .patch ("https://{}{}" .format (auth ["host" ], path ), headers = auth ['headers' ],
173
+ json = body , proxies = proxies )
132
174
res .raise_for_status ()
133
175
except requests .exceptions .RequestException as e :
134
176
if e .response .text :
@@ -143,29 +185,22 @@ def get_build(auth, build_id):
143
185
144
186
145
187
def get_builds (auth , source ):
146
- path = "/api/v1/deployment/builds/?workspaceName={}&model_version_id={}" .format (source ['workspace' ], source ['model_version_id' ])
188
+ path = "/api/v1/deployment/builds/?workspaceName={}&model_version_id={}" .format (
189
+ source ['workspace_name' ], source ['model_version_id' ])
190
+ print (f"\n \n PATH = { path } \n \n " )
191
+ builds = get (auth , path )
192
+ print (f"\n \n BUILDS = { builds } \n \n " )
147
193
return get (auth , path )
148
194
149
195
150
196
def get_model_version (auth , model_version_id ):
151
- return get (auth , '/api/v1/registry/model_versions/{}' .format (model_version_id ))['model_version' ]
197
+ return get (auth , '/api/v1/registry/model_versions/{}' .format (model_version_id ))[
198
+ 'model_version' ]
152
199
153
200
154
201
def get_registered_model (auth , registered_model_id ):
155
- return get (auth , '/api/v1/registry/registered_models/{}' .format (registered_model_id ))['registered_model' ]
156
-
157
-
158
- def get_registered_models_by_name (auth , registered_model_name ):
159
- path = '/api/v1/registry/workspaces/{}/registered_models/find' .format (auth ['workspace' ])
160
- predicates = {
161
- 'predicates' : [{
162
- "key" : "name" ,
163
- "operator" : "EQ" ,
164
- "value" : registered_model_name ,
165
- "value_type" : "STRING"
166
- }]
167
- }
168
- return post (auth , path , predicates )['registered_models' ]
202
+ return get (auth , '/api/v1/registry/registered_models/{}' .format (registered_model_id ))[
203
+ 'registered_model' ]
169
204
170
205
171
206
def signed_artifact_url (auth , model_version_id , artifact ):
@@ -193,7 +228,8 @@ def download_artifact(auth, model_version_id, artifact):
193
228
key = artifact ['key' ]
194
229
url = signed_artifact_url (auth , model_version_id , artifact )
195
230
print ("Downloading artifact '%s'" % key )
196
- curl_cmd = "curl %s -o %s '%s'" % (params ['VERTA_CURL_OPTS' ], key , url )
231
+ curl_cmd = "curl --cacert %s -o %s %s '%s'" % (
232
+ os .environ ['REQUESTS_CA_BUNDLE' ], key , params ['VERTA_CURL_OPTS' ], url )
197
233
os .system (curl_cmd )
198
234
199
235
@@ -208,7 +244,8 @@ def download_artifacts(auth, model_version_id, artifacts, model_artifact):
208
244
}
209
245
copy_fields (['artifact_type' , 'key' ], artifact , artifact_request )
210
246
download_artifact (auth , model_version_id , artifact_request )
211
- downloaded_artifacts .append ({'key' : artifact ['key' ], 'artifact_type' : artifact ['artifact_type' ]})
247
+ downloaded_artifacts .append (
248
+ {'key' : artifact ['key' ], 'artifact_type' : artifact ['artifact_type' ]})
212
249
213
250
model_artifact_request = {
214
251
'method' : 'GET' ,
@@ -223,23 +260,31 @@ def download_artifacts(auth, model_version_id, artifacts, model_artifact):
223
260
def upload_artifact (auth , model_version_id , artifact ):
224
261
key = artifact ['key' ]
225
262
print ("Uploading artifact '%s'" % key )
226
-
263
+ print ( artifact )
227
264
artifact_request = {
228
265
'method' : 'PUT' ,
229
266
'model_version_id' : model_version_id ,
230
267
'key' : key
231
268
}
232
269
put_url = signed_artifact_url (auth , model_version_id , artifact_request )
233
270
data = open (key , 'rb' )
234
- put_response = requests .put (put_url , data = data , headers = {'Content-type' : 'application/octet-stream' })
271
+ headers_dict = {
272
+ 'Grpc-metadata-source' : 'PythonClient' ,
273
+ 'Content-type' : 'application/octet-stream' ,
274
+ 'Grpc-metadata-email' : os .environ ['VERTA_DEST_EMAIL' ],
275
+ 'Grpc-metadata-developer_key' : os .environ ['VERTA_DEST_DEV_KEY' ]
276
+ }
277
+ put_response = requests .put (put_url , data = data , headers = headers_dict )
235
278
236
279
if not put_response .ok :
237
- raise Exception ("Failed to put artifact (%d %s). Key: %s\t URL: %s\t Text: %s" % (put_response .status_code ,
238
- put_response .reason , key , put_url , put_response .text ))
239
-
240
- check_url = signed_artifact_url (auth , model_version_id , {'method' : 'GET' , 'model_version_id' : model_version_id ,
241
- 'key' : key })
242
- check = requests .get (check_url )
280
+ raise Exception ("Failed to put artifact (%d %s). Key: %s\t URL: %s\t Text: %s" % (
281
+ put_response .status_code ,
282
+ put_response .reason , key , put_url , put_response .text ))
283
+
284
+ check_url = signed_artifact_url (auth , model_version_id ,
285
+ {'method' : 'GET' , 'model_version_id' : model_version_id ,
286
+ 'key' : key })
287
+ check = requests .get (check_url , headers = headers_dict )
243
288
if not check .ok :
244
289
raise Exception ("Failed to verify artifact '%s' upload at URL %s" % (key , check_url ))
245
290
@@ -263,7 +308,8 @@ def get_promotion_data(_config):
263
308
model_version_id = source ['model_version_id' ]
264
309
265
310
print ("Fetching promotion data for model version %d" % source ['model_version_id' ])
266
- source_auth = auth_context (source ['host' ], source ['email' ], source ['devkey' ], source ['workspace' ])
311
+ source_auth = auth_context (source ['host' ], source ['email' ], source ['devkey' ],
312
+ source ['workspace' ])
267
313
model_version = get_model_version (source_auth , model_version_id )
268
314
269
315
all_builds = get_builds (source_auth , source )
@@ -273,24 +319,28 @@ def get_promotion_data(_config):
273
319
build = None
274
320
latest_date = None
275
321
for b in all_builds ['builds' ]:
276
- if 'self_contained' in b ['creator_request' ] and b ['creator_request' ]['self_contained' ]:
322
+ print (f"\n \n BUILDS = { b } \n \n " )
323
+ if 'self_contained' in b ['creator_request' ]:
277
324
build_date = datetime .datetime .strptime (b ['date_created' ], time_format )
278
325
if not latest_date or build_date > latest_date :
279
326
latest_date = build_date
280
327
build = b
281
328
282
329
if not build or not latest_date :
283
- print ("No self contained builds found for model version id %d, promotion stopped." % source ['model_version_id' ])
330
+ print (
331
+ "No self contained builds found for model version id %d, promotion stopped." % source [
332
+ 'model_version_id' ])
284
333
raise SystemExit (1 )
285
334
286
- model = get_registered_model (source_auth , model_version ['registered_model_id' ])
287
- artifacts = download_artifacts (source_auth , model_version_id , model_version ['artifacts' ], model_version ['model' ])
335
+ model = get_registered_model (source_auth , model_version ['registered_model_id' ])
336
+ artifacts = download_artifacts (source_auth , model_version_id , model_version ['artifacts' ],
337
+ model_version ['model' ])
288
338
289
339
promotion = {
290
- 'build' : build ,
291
- 'model_version' : model_version ,
292
- 'model' : model ,
293
- 'artifacts' : artifacts
340
+ 'build' : build ,
341
+ 'model_version' : model_version ,
342
+ 'model' : model ,
343
+ 'artifacts' : artifacts
294
344
}
295
345
return promotion
296
346
@@ -301,7 +351,8 @@ def create_model(auth, source_model, source_artifacts):
301
351
model = {
302
352
'artifacts' : source_artifacts
303
353
}
304
- copy_fields (['labels' , 'custom_permission' , 'name' , 'readme_text' , 'resource_visibility' , 'description' ], source_model , model )
354
+ copy_fields (['labels' , 'custom_permission' , 'name' , 'readme_text' , 'resource_visibility' ,
355
+ 'description' ], source_model , model )
305
356
return post (auth , path , model )['registered_model' ]
306
357
307
358
@@ -312,15 +363,17 @@ def create_model_version(auth, source_model_version, promoted_model):
312
363
if 'labels' in source_model_version .keys ():
313
364
model_version ['labels' ] = source_model_version ['labels' ]
314
365
315
- fields = ['artifacts' , 'attributes' , 'environment' , 'version' , 'readme_text' , 'model' , 'description' , 'labels' ]
366
+ fields = ['artifacts' , 'attributes' , 'environment' , 'version' , 'readme_text' , 'model' ,
367
+ 'description' , 'labels' ]
316
368
copy_fields (fields , source_model_version , model_version )
317
369
return post (auth , path , model_version )['model_version' ]
318
370
319
371
320
372
def patch_model (auth , registered_model_id , model_version_id , model ):
321
373
print ("Updating model artifact for model version '%s'" % model_version_id )
322
374
323
- path = '/api/v1/registry/registered_models/{}/model_versions/{}' .format (registered_model_id , model_version_id )
375
+ path = '/api/v1/registry/registered_models/{}/model_versions/{}' .format (registered_model_id ,
376
+ model_version_id )
324
377
update = {'model' : model }
325
378
return patch (auth , path , update )
326
379
@@ -360,21 +413,15 @@ def upload_build(source_build):
360
413
361
414
def create_promotion (_config , promotion ):
362
415
dest = _config ['dest' ]
363
-
416
+
364
417
dest_auth = auth_context (dest ['host' ], dest ['email' ], dest ['devkey' ], dest ['workspace' ])
365
418
366
419
print ("Starting promotion" )
367
420
build_location = upload_build (promotion ['build' ])
368
- if not dest ['registered_model_name ' ]:
421
+ if not dest ['registered_model_id ' ]:
369
422
model = create_model (dest_auth , promotion ['model' ], promotion ['artifacts' ])
370
423
else :
371
- models = get_registered_models_by_name (dest_auth , dest ['registered_model_name' ])
372
- if len (models ) > 1 :
373
- print ("WARNING: Multiple registered models with name '%s' found, using first one with id '%s'" % (dest ['registered_model_name' ], models [0 ]["id" ]))
374
- elif len (models ) == 0 :
375
- print ("ERROR: Registered model with name '%s' not found" % dest ['registered_model_name' ])
376
- return
377
- model = models [0 ]
424
+ model = get_registered_model (dest_auth , dest ['registered_model_id' ])
378
425
print ("Using existing registered model '%s'" % model ['name' ])
379
426
model_version = create_model_version (dest_auth , promotion ['model_version' ], model )
380
427
0 commit comments