Skip to content

Commit 9c338cb

Browse files
author
Heather MacDonald
committed
chore: Catch promotion script up with Allstate's version
1 parent ceaa558 commit 9c338cb

File tree

1 file changed

+114
-67
lines changed

1 file changed

+114
-67
lines changed

deployment/promotion/promote.py

Lines changed: 114 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,24 @@
22
"""
33
This is a Python script that will copy/promote a Verta Build from one environment
44
to another.
5-
65
- The script will use the model version passed in the VERTA_SOURCE_MODEL_VERSION_ID environment variable
76
as the model version to promote.
87
- The latest self-contained build of the model version will be promoted. The promotion process will terminate if no
98
self-contained builds of the model version are found.
109
- 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:
1411
- VERTA_SOURCE_MODEL_VERSION_ID: The ID of the model version to promote
1512
- VERTA_SOURCE_HOST: The source Verta instance to promote from
1613
- VERTA_SOURCE_EMAIL: The email address for authentication to the source Verta instance
1714
- 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
1916
- VERTA_DEST_HOST: The destination Verta instance to promote to
2017
- VERTA_DEST_EMAIL: The email address for authentication to the destination Verta instance
2118
- 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
2521
Optional environment variables to configure curl usage:
2622
VERTA_CURL_OPTS: Options to pass to curl. Defaults to '-O'
27-
2823
With these values set, run the script. The script will not attempt to delete any data and will fail if the registered
2924
model (if an existing one has not been provided) or version already exists in the destination.
3025
"""
@@ -35,12 +30,52 @@
3530
import os
3631
import datetime
3732

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',
4037
'VERTA_DEST_WORKSPACE']
41-
opt_env_vars = ['VERTA_DEST_REGISTERED_MODEL']
38+
39+
opt_env_vars = ['VERTA_DEST_REGISTERED_MODEL_ID']
40+
4241
params = {}
4342

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']
4479

4580
for param_name in env_vars:
4681
param = os.environ.get(param_name)
@@ -57,21 +92,23 @@
5792
params['VERTA_CURL_OPTS'] = curl_opts
5893
else:
5994
params['VERTA_CURL_OPTS'] = ''
95+
params['VERTA_CURL_OPTS'] += f' -H @curl_headers'
6096

6197
config = {
6298
'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]),
64100
'host': params['VERTA_SOURCE_HOST'],
65101
'email': params['VERTA_SOURCE_EMAIL'],
66102
'devkey': params['VERTA_SOURCE_DEV_KEY'],
67-
'workspace': params['VERTA_SOURCE_WORKSPACE']
103+
'workspace': params['VERTA_SOURCE_WORKSPACE_0'],
104+
'workspace_name': source_workspace
68105
},
69106
'dest': {
70107
'host': params['VERTA_DEST_HOST'],
71108
'email': params['VERTA_DEST_EMAIL'],
72109
'devkey': params['VERTA_DEST_DEV_KEY'],
73110
'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']
75112
}
76113
}
77114

@@ -84,7 +121,8 @@ def copy_fields(fields, src, dest):
84121

85122
def auth_context(host, email, devkey, workspace):
86123
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,
88126
'workspace': workspace
89127
}
90128

@@ -93,7 +131,8 @@ def post(auth, path, body):
93131
auth['headers']['Content-Type'] = 'application/json'
94132
body['workspaceName'] = auth['workspace']
95133
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)
97136
res.raise_for_status()
98137
except requests.exceptions.RequestException as e:
99138
if e.response.text:
@@ -104,7 +143,8 @@ def post(auth, path, body):
104143

105144
def get(auth, path):
106145
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)
108148
res.raise_for_status()
109149
except requests.exceptions.RequestException as e:
110150
if e.response.text:
@@ -115,7 +155,8 @@ def get(auth, path):
115155

116156
def put(auth, path, body):
117157
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)
119160
res.raise_for_status()
120161
except requests.exceptions.RequestException as e:
121162
if e.response.text:
@@ -128,7 +169,8 @@ def put(auth, path, body):
128169

129170
def patch(auth, path, body):
130171
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)
132174
res.raise_for_status()
133175
except requests.exceptions.RequestException as e:
134176
if e.response.text:
@@ -143,29 +185,22 @@ def get_build(auth, build_id):
143185

144186

145187
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\nPATH = {path}\n\n")
191+
builds = get(auth, path)
192+
print(f"\n\nBUILDS = {builds}\n\n")
147193
return get(auth, path)
148194

149195

150196
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']
152199

153200

154201
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']
169204

170205

171206
def signed_artifact_url(auth, model_version_id, artifact):
@@ -193,7 +228,8 @@ def download_artifact(auth, model_version_id, artifact):
193228
key = artifact['key']
194229
url = signed_artifact_url(auth, model_version_id, artifact)
195230
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)
197233
os.system(curl_cmd)
198234

199235

@@ -208,7 +244,8 @@ def download_artifacts(auth, model_version_id, artifacts, model_artifact):
208244
}
209245
copy_fields(['artifact_type', 'key'], artifact, artifact_request)
210246
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']})
212249

213250
model_artifact_request = {
214251
'method': 'GET',
@@ -223,23 +260,31 @@ def download_artifacts(auth, model_version_id, artifacts, model_artifact):
223260
def upload_artifact(auth, model_version_id, artifact):
224261
key = artifact['key']
225262
print("Uploading artifact '%s'" % key)
226-
263+
print(artifact)
227264
artifact_request = {
228265
'method': 'PUT',
229266
'model_version_id': model_version_id,
230267
'key': key
231268
}
232269
put_url = signed_artifact_url(auth, model_version_id, artifact_request)
233270
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)
235278

236279
if not put_response.ok:
237-
raise Exception("Failed to put artifact (%d %s). Key: %s\tURL: %s\tText: %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\tURL: %s\tText: %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)
243288
if not check.ok:
244289
raise Exception("Failed to verify artifact '%s' upload at URL %s" % (key, check_url))
245290

@@ -263,7 +308,8 @@ def get_promotion_data(_config):
263308
model_version_id = source['model_version_id']
264309

265310
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'])
267313
model_version = get_model_version(source_auth, model_version_id)
268314

269315
all_builds = get_builds(source_auth, source)
@@ -273,24 +319,28 @@ def get_promotion_data(_config):
273319
build = None
274320
latest_date = None
275321
for b in all_builds['builds']:
276-
if 'self_contained' in b['creator_request'] and b['creator_request']['self_contained']:
322+
print(f"\n\nBUILDS = {b}\n\n")
323+
if 'self_contained' in b['creator_request']:
277324
build_date = datetime.datetime.strptime(b['date_created'], time_format)
278325
if not latest_date or build_date > latest_date:
279326
latest_date = build_date
280327
build = b
281328

282329
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'])
284333
raise SystemExit(1)
285334

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'])
288338

289339
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
294344
}
295345
return promotion
296346

@@ -301,7 +351,8 @@ def create_model(auth, source_model, source_artifacts):
301351
model = {
302352
'artifacts': source_artifacts
303353
}
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)
305356
return post(auth, path, model)['registered_model']
306357

307358

@@ -312,15 +363,17 @@ def create_model_version(auth, source_model_version, promoted_model):
312363
if 'labels' in source_model_version.keys():
313364
model_version['labels'] = source_model_version['labels']
314365

315-
fields = ['artifacts', 'attributes', 'environment', 'version', 'readme_text', 'model', 'description', 'labels']
366+
fields = ['artifacts', 'attributes', 'environment', 'version', 'readme_text', 'model',
367+
'description', 'labels']
316368
copy_fields(fields, source_model_version, model_version)
317369
return post(auth, path, model_version)['model_version']
318370

319371

320372
def patch_model(auth, registered_model_id, model_version_id, model):
321373
print("Updating model artifact for model version '%s'" % model_version_id)
322374

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)
324377
update = {'model': model}
325378
return patch(auth, path, update)
326379

@@ -360,21 +413,15 @@ def upload_build(source_build):
360413

361414
def create_promotion(_config, promotion):
362415
dest = _config['dest']
363-
416+
364417
dest_auth = auth_context(dest['host'], dest['email'], dest['devkey'], dest['workspace'])
365418

366419
print("Starting promotion")
367420
build_location = upload_build(promotion['build'])
368-
if not dest['registered_model_name']:
421+
if not dest['registered_model_id']:
369422
model = create_model(dest_auth, promotion['model'], promotion['artifacts'])
370423
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'])
378425
print("Using existing registered model '%s'" % model['name'])
379426
model_version = create_model_version(dest_auth, promotion['model_version'], model)
380427

0 commit comments

Comments
 (0)