From 437fa140ce3bc06c24d8ea3de580ba47675ffbb9 Mon Sep 17 00:00:00 2001 From: Cameron Pierce Date: Mon, 2 Oct 2017 15:31:10 -0700 Subject: [PATCH 1/5] Feedback used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit changed file names, added features of Django app, switched to r.status_code per Dmitry’s feedback --- .../{initFunctions.py => bulk_functions.py} | 25 ++-- .../{initTemplate.py => bulk_initiate.py} | 121 +++++++++--------- 2 files changed, 77 insertions(+), 69 deletions(-) rename Workflow_via_CSV/{initFunctions.py => bulk_functions.py} (79%) rename Workflow_via_CSV/{initTemplate.py => bulk_initiate.py} (61%) diff --git a/Workflow_via_CSV/initFunctions.py b/Workflow_via_CSV/bulk_functions.py similarity index 79% rename from Workflow_via_CSV/initFunctions.py rename to Workflow_via_CSV/bulk_functions.py index cb676fe..caf2acf 100644 --- a/Workflow_via_CSV/initFunctions.py +++ b/Workflow_via_CSV/bulk_functions.py @@ -1,6 +1,6 @@ # initFunctions.py # written and tested in Python 3.6.0 -# last updated 07/28/17 +# last updated 10/02/17 import requests import json @@ -19,25 +19,26 @@ def getToken(url_root, username, password, client_id, client_secret): # create dict for body body = {'grant_type' : 'password', - 'scope' : 'api', - 'redirect_uri' : 'testuri', - 'username' : username, - 'password' : password, - 'client_id' : client_id, - 'client_secret' : client_secret} + 'scope' : 'api', + 'redirect_uri' : 'testuri', + 'username' : username, + 'password' : password, + 'client_id' : client_id, + 'client_secret' : client_secret} # return POST call response return requests.post(url, headers=headers, data=body) -def getTemplateID(url_root, token): +def getTemplateID(url_root, workflow_name, token): """ Given: URL root, name of workflow, and valid token. Return: Response of GET call for ID of template. """ # construct URL - # choosing not to filter here, characters like '&' can cause problems - url = ("{}api/v1/templates/dashboard".format(url_root)) + # filtering here, but characters like '&' can cause problems + url = ("{}api/v1/templates/dashboard?$filter=WorkflowName eq '{}'" + .format(url_root, workflow_name)) # needs token headers = {'Authorization' : 'Bearer {}'.format(token)} @@ -56,7 +57,7 @@ def getFormInfo(url_root, template_id, token): # needs token headers = {'Authorization' : 'Bearer {}'.format(token), - 'Content-Type' : 'application/json'} + 'Content-Type' : 'application/json'} # return GET call response return requests.get(url, headers=headers) @@ -72,7 +73,7 @@ def initiateWorkflow(url_root, template_id, token, body): # needs token headers = {'Authorization' : 'Bearer {}'.format(token), - 'Content-Type' : 'application/json'} + 'Content-Type' : 'application/json'} # encode body into JSON json_body = json.dumps(body) diff --git a/Workflow_via_CSV/initTemplate.py b/Workflow_via_CSV/bulk_initiate.py similarity index 61% rename from Workflow_via_CSV/initTemplate.py rename to Workflow_via_CSV/bulk_initiate.py index b7e4f7b..69a99fe 100644 --- a/Workflow_via_CSV/initTemplate.py +++ b/Workflow_via_CSV/bulk_initiate.py @@ -1,13 +1,13 @@ # initTemplate.py # written and tested in Python 3.6.0 -# last updated 07/28/17 +# last updated 10/02/17 """ This script initiates workflows using field data from a CSV file. The first row of the CSV should contain field labels, and all other rows should hold field values (each row will be a separate instance of the workflow). The script -creates a new txt file (in the same directory as this file) each time it is run, -and writes the result of each workflow instance, including error messages. +creates a new txt file (in same directory as this) each time it is run, and +writes the result of each workflow instance, including error messages. To use, create a workflow, create a CSV based on the workflow, and update the global variables below. If you find bugs or errors that are not handled, @@ -18,8 +18,8 @@ import time import os import sys +from bulk_functions import * import json -from resource_owner import * import csv from collections import defaultdict @@ -27,8 +27,8 @@ # Global Variables # # ---------------- # -# replace tenant with your own -url_root = 'https://tenant.tap.thinksmart.com/prod/' +# replace tenant, environment, instance with your own +url_root = 'https://tenant.environment.thinksmart.com/instance/' # fill in (must be strings) client_id = '' client_secret = '' @@ -38,9 +38,9 @@ username = input("Username: ") password = getpass.getpass() -# ---------------------------- # -# Start Timer, Create txt File # -# ---------------------------- # +# -------------------------- # +# Start Timer, Create Report # +# -------------------------- # # get start time t0 = time.time() @@ -50,29 +50,32 @@ while os.path.exists('{}{}.txt'.format(workflow_name, i)): i += 1 -# create txt file, write header +# create txt file, write timestamp f = open('{}{}.txt'.format(workflow_name, i), 'w') f.write(time.asctime() + "\n\n") -f.write("Submitting {} with field values from {}...\n\n" - .format(workflow_name, csv_name)) # --------- # # Get Token # # --------- # +# get time of first token (valid 3600 seconds) +token_timer = time.time() + # get token response r = getToken(url_root, username, password, client_id, client_secret) # invalid url_root causes 404 response -if (str(r) == ''): +if (r.status_code == 404): f.write("Invalid url_root") sys.exit() -elif (str(r) == ''): +elif (r.status_code == 400): # invalid user credentials or client info causes 400 response if (json.loads(r.text).get('error') == 'invalid_grant'): - f.write("Invalid username or password") + f.write("Invalid Username and/or Password.") + elif (json.loads(r.text).get('error') == 'unauthorized_client'): + f.write("Valid client_id and client_secret, invalid authentication flow.") else: - f.write("Invalid client_id or client_secret") + f.write("Invalid client_id and/or client_secret.") sys.exit() # decode JSON, get token @@ -83,19 +86,19 @@ # --------------- # # get template ID response -r = getTemplateID(url_root, token) - -# search for template -template_id = None -for w in json.loads(r.text).get('Items'): - if (w.get('WorkflowName') == workflow_name): - template_id = w.get('ID') - break - -# if template not found, report error -if (not template_id): - f.write("Invalid workflow_name (has a field been added to the repository?)") - sys.exit() +r = getTemplateID(url_root, workflow_name, token) + +# parse to where ID should be +temp = json.loads(r.text).get('Items') + +# if empty in that location, write and exit +if (not temp): + f.write("Invalid workflow_name. This may occur when {} {}" + .format("the name contains non-alphabetic characters or", + "none of the workflow fields have been added to the repository.")) +# else assign it +else: + template_id = temp[0].get('ID') # -------- # # Read CSV # @@ -115,7 +118,7 @@ try: csv_labels = next(csv_fields) # encoding may cause UnicodeDecodeError -except UnicodeDecodeError as e: +except UnicodeDecodeError: f.write("Please use a .csv with UTF-8 encoding.") sys.exit() @@ -140,42 +143,44 @@ # hold labels and names that are validated with d labels, names = [], [] i = 0 -# use a while loop here, because popping labels inside for loop will skip some +# use a while loop b/c popping labels inside for loop will skip some while (i < len(csv_labels)): - # v = value for key from csv_labels, False if not present + # v = value for key from csv_labels v = d.get(csv_labels[i], False) - # if v is True (k is False if key not present, OR list empty) + # if v is True (is False if key not present) if (v): + current = csv_labels.pop(i) # append now-validated name and label to lists - labels.append("{} ({})".format(csv_labels.pop(i), v[0])) + labels.append("{} ({})".format(current, v[0])) names.append(v.pop(0)) + # if v is empty, pop key (lower time complexity w/ for loop below) + if (len(v) == 0): + d.pop(current) else: i += 1 # gather remaining fields from d remainder = [] -# for each key, if value list is not empty... +# for each remaining key... for k, v in d.items(): - if (v): - # if value has multiple values, put list in parens - if (len(v) > 1): - remainder.append("{} ({})".format(k, ", ".join(v))) - # else put single value in parens - else: - remainder.append("{} ({})".format(k, v[0])) + # if value has multiple values, put list in parens + if (len(v) > 1): + remainder.append("{} ({})".format(k, ", ".join(v))) + # else put single value in parens + else: + remainder.append("{} ({})".format(k, v[0])) # report on comparison mismatch = "Labels from {} that do not match {}:\n{}\n\n" -if ((not csv_labels) and (not remainder)): - f.write("All field names from {} and {} match\n\n" - .format(csv_name, workflow_name)) +if ((not remainder) and (not csv_labels)): + pass elif (not remainder): - f.write(mismatch.format(csv_name, workflow_name, "\n".join(csv_labels))) + f.append(mismatch.format(csv_name, workflow_name, "\n".join(csv_labels))) elif (not csv_labels): - f.write(mismatch.format(workflow_name, csv_name, "\n".join(remainder))) + f.append(mismatch.format(workflow_name, csv_name, "\n".join(remainder))) else: - f.write(mismatch.format(csv_name, workflow_name, "\n".join(csv_labels))) - f.write(mismatch.format(workflow_name, csv_name, "\n".join(remainder))) + f.append(mismatch.format(csv_name, workflow_name, "\n".join(csv_labels))) + f.append(mismatch.format(workflow_name, csv_name, "\n".join(remainder))) # ------------------ # # Initiate Workflows # @@ -183,12 +188,16 @@ # count var for reporting submit_count = 0 + # for rows below first, construct body and make API call for row, field_vals in enumerate(csv_fields, 2): + # refresh token if needed - if ((time.time()-t0) > 3600): + if ((time.time()-token_timer) > 3500): + token_timer = time.time() r = getToken(url_root, username, password, client_id, client_secret) token = json.loads(r.text).get('access_token') + # clear body body = {} # fill body @@ -197,13 +206,11 @@ # initiate workflow r = initiateWorkflow(url_root, template_id, token, body) # empty required field, unmatched dropdown value, etc. causes 400 response - if (str(r) == ''): - f.write("Row {}: Error\n".format(row)) - f.write("------------" + "\n") - f.write(str(r.text) + "\n\n") + if (r.status_code == 400): + f.write("----------\nRow {}: Error\n----------\n").format(row) + f.write("{}\n\n".format(r.text)) else: - f.write("Row {}: Submitted\n".format(row)) - f.write("----------------" + "\n") + f.write("----------\nRow {}: Submitted\n----------\n").format(row) for i, val in enumerate(body): f.write("{} : {}\n".format(labels[i], body[val])) f.write("\n") @@ -211,5 +218,5 @@ # report on submits and errors, time f.write("{} submitted on {} of {} tries\n" - .format(workflow_name, submit_count, row-1)) + .format(workflow_name, submit_count, row-1)) f.write("Time elapsed: {}s".format(round(time.time()-t0, 3))) From 1259d6088fca046524da857d77b5d3cd0da8f658 Mon Sep 17 00:00:00 2001 From: Cameron Pierce Date: Mon, 2 Oct 2017 15:42:36 -0700 Subject: [PATCH 2/5] Header comment update --- Workflow_via_CSV/bulk_functions.py | 2 +- Workflow_via_CSV/bulk_initiate.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Workflow_via_CSV/bulk_functions.py b/Workflow_via_CSV/bulk_functions.py index caf2acf..69e1fae 100644 --- a/Workflow_via_CSV/bulk_functions.py +++ b/Workflow_via_CSV/bulk_functions.py @@ -1,4 +1,4 @@ -# initFunctions.py +# bulk_functions.py # written and tested in Python 3.6.0 # last updated 10/02/17 diff --git a/Workflow_via_CSV/bulk_initiate.py b/Workflow_via_CSV/bulk_initiate.py index 69a99fe..714d9bb 100644 --- a/Workflow_via_CSV/bulk_initiate.py +++ b/Workflow_via_CSV/bulk_initiate.py @@ -1,4 +1,4 @@ -# initTemplate.py +# bulk_initiate.py # written and tested in Python 3.6.0 # last updated 10/02/17 From 9ef909fb06a5a7ee832b2530e5f8dc3b7bd6db0d Mon Sep 17 00:00:00 2001 From: Cameron Pierce Date: Mon, 2 Oct 2017 18:08:23 -0700 Subject: [PATCH 3/5] Submitting WFs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Global vars changed, it’s working, check that it catches all errors tomorrow --- Workflow_via_CSV/bulk_functions.py | 2 +- Workflow_via_CSV/bulk_initiate.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Workflow_via_CSV/bulk_functions.py b/Workflow_via_CSV/bulk_functions.py index 69e1fae..c61feb6 100644 --- a/Workflow_via_CSV/bulk_functions.py +++ b/Workflow_via_CSV/bulk_functions.py @@ -1,5 +1,5 @@ # bulk_functions.py -# written and tested in Python 3.6.0 +# written and tested in Python 3.6.2 # last updated 10/02/17 import requests diff --git a/Workflow_via_CSV/bulk_initiate.py b/Workflow_via_CSV/bulk_initiate.py index 714d9bb..e673960 100644 --- a/Workflow_via_CSV/bulk_initiate.py +++ b/Workflow_via_CSV/bulk_initiate.py @@ -1,5 +1,5 @@ # bulk_initiate.py -# written and tested in Python 3.6.0 +# written and tested in Python 3.6.2 # last updated 10/02/17 """ @@ -28,12 +28,12 @@ # ---------------- # # replace tenant, environment, instance with your own -url_root = 'https://tenant.environment.thinksmart.com/instance/' +url_root = 'https://default.stagingtap.thinksmart.com/default/' # fill in (must be strings) -client_id = '' -client_secret = '' -workflow_name = '' -csv_name = '' +client_id = 'b473d2ace41144bb87d63be7584ecec1' +client_secret = 'Yjr1ZnzMgN58x7H+WlPM6WRTzk1B7RocuYkOEuVhcpw=' +workflow_name = 'PyScript Test' +csv_name = '/Users/cameronpierce/Dropbox/thinksmart/api/PyScript.csv' # enter username and password in command line during runtime username = input("Username: ") password = getpass.getpass() @@ -175,12 +175,12 @@ if ((not remainder) and (not csv_labels)): pass elif (not remainder): - f.append(mismatch.format(csv_name, workflow_name, "\n".join(csv_labels))) + f.write(mismatch.format(csv_name, workflow_name, "\n".join(csv_labels))) elif (not csv_labels): - f.append(mismatch.format(workflow_name, csv_name, "\n".join(remainder))) + f.write(mismatch.format(workflow_name, csv_name, "\n".join(remainder))) else: - f.append(mismatch.format(csv_name, workflow_name, "\n".join(csv_labels))) - f.append(mismatch.format(workflow_name, csv_name, "\n".join(remainder))) + f.write(mismatch.format(csv_name, workflow_name, "\n".join(csv_labels))) + f.write(mismatch.format(workflow_name, csv_name, "\n".join(remainder))) # ------------------ # # Initiate Workflows # @@ -207,10 +207,10 @@ r = initiateWorkflow(url_root, template_id, token, body) # empty required field, unmatched dropdown value, etc. causes 400 response if (r.status_code == 400): - f.write("----------\nRow {}: Error\n----------\n").format(row) + f.write("{0}\nRow {1}: Error\n{0}\n".format("-"*(len(str(row))+11),row)) f.write("{}\n\n".format(r.text)) else: - f.write("----------\nRow {}: Submitted\n----------\n").format(row) + f.write("{0}\nRow {1}: Submitted\n{0}\n".format("-"*(len(str(row))+15),row)) for i, val in enumerate(body): f.write("{} : {}\n".format(labels[i], body[val])) f.write("\n") From 00c01628f91e25d29fe7ffc3001301da78e4c6a4 Mon Sep 17 00:00:00 2001 From: Cameron Pierce Date: Mon, 9 Oct 2017 10:16:45 -0700 Subject: [PATCH 4/5] Just comments --- Workflow_via_CSV/bulk_initiate.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Workflow_via_CSV/bulk_initiate.py b/Workflow_via_CSV/bulk_initiate.py index e673960..a35c361 100644 --- a/Workflow_via_CSV/bulk_initiate.py +++ b/Workflow_via_CSV/bulk_initiate.py @@ -3,11 +3,11 @@ # last updated 10/02/17 """ -This script initiates workflows using field data from a CSV file. The first -row of the CSV should contain field labels, and all other rows should hold -field values (each row will be a separate instance of the workflow). The script -creates a new txt file (in same directory as this) each time it is run, and -writes the result of each workflow instance, including error messages. +This script initiates workflows using data from a CSV file. The first row of +the CSV should contain field labels, and all other rows should hold field +values (each row will be a separate instance of the workflow). The script +creates a txt file (in same directory as this) each time it is run, and writes +the result of each workflow instance, including error messages. To use, create a workflow, create a CSV based on the workflow, and update the global variables below. If you find bugs or errors that are not handled, @@ -126,6 +126,10 @@ # Convert Field Labels to Names # # ----------------------------- # +# features to add here: + # accept names and labels + # accept labels that meet word-similarity threshold (in event of typo) + # get form info from workflow r = getFormInfo(url_root, template_id, token) From 0032b59482c9c219264b99f9831aacd7f2c96629 Mon Sep 17 00:00:00 2001 From: Cameron Pierce Date: Mon, 9 Oct 2017 13:11:39 -0700 Subject: [PATCH 5/5] Restructuring Readme part 1 --- Workflow_via_CSV/README.md | 54 ++++++++++++++++++++++++++++--- Workflow_via_CSV/bulk_initiate.py | 15 +++++---- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/Workflow_via_CSV/README.md b/Workflow_via_CSV/README.md index 96642e1..8747249 100644 --- a/Workflow_via_CSV/README.md +++ b/Workflow_via_CSV/README.md @@ -1,9 +1,55 @@ # Workflow_via_CSV -The files in this folder can be used for "batch processing" of a workflow, i.e. submitting the initial stage of a workflow any number of times, with potentially different field values in each instance. Specifically, [Initiate_Template.py](https://github.com/ThinkSmart/API_Examples/blob/master/Workflow_via_CSV/Initiate_Template.py?ts=2) retrieves an access token via resource owner flow, gets the template ID for the workflow of interest, and uses data in a CSV file to initiate workflows. Calls to the ThinkSmart API are done with functions from [Initiate_Functions.py](https://github.com/ThinkSmart/API_Examples/blob/master/Workflow_via_CSV/Initiate_Functions.py?ts=2), which are imported to the template file. This was done for readability and reusability. +The files in this folder can be used for bulk workflow initiation, i.e. submitting the first stage of a workflow any number of times, with potentially different field values in each instance. Specifically, [bulk_initiate.py](https://github.com/ThinkSmart/API_Examples/blob/master/Workflow_via_CSV/bulk_initiate.py?ts=2) retrieves an access token via resource owner flow, finds the template ID for the workflow of interest, and uses data in a CSV file to initiate workflows. Calls to the ThinkSmart API are made with functions imported from [bulk_functions.py](https://github.com/ThinkSmart/API_Examples/blob/master/Workflow_via_CSV/bulk_functions.py?ts=2). -Although [Example1.py](https://github.com/ThinkSmart/API_Examples/blob/master/Example1.py?ts=2) also uses resource owner flow to initiate a workflow, the files here offer a bit more functionality. Namely, field values come from a CSV (rather than the script itself), and a particular workflow can be initiated more than once because the script loops through each row of the CSV. Also, the template file creates a new txt file (in the same directory) each time it's run, and writes the result of each submission, including error messages. +To get started, here's what to do. + +1. First, build your workflow in TAP -The script can detect a number of errors. First, it checks that all the global variables are valid. Any errors there will cause the script to quit (the ability to get a token, template ID, or data from the CSV is disrupted). It's also possible for errors to occur with certain rows of the CSV (i.e. required field is empty, value doesn't match options in dropdown, etc.) The script won't quit in that case, but that row won't be submitted successfully. Lastly, the script checks whether the field names in the workflow match the field names in the CSV. If any of these errors occur, they'll appear in the txt file. +Preferably avoid punctuation in the workflow name + Also be sure it's been published and saved -To get started, create a workflow in TAP and be sure to leave the field names unchanged (i.e. element1). Create a CSV (preferably UTF-8 encoded) based on your workflow, where the first row is field names and subsequent rows are field values. Then, update the global variables in the template file. This includes the client ID and secret for your tenant, as well as the name of your workflow and CSV. Make sure your CSV is in the same directory as the template and function files. You'll be prompted to enter your TAP username and password during runtime. +2. Then, create a CSV file based on your workflow + +In the first row of your CSV file, add the labels of the fields shown in the first stage of your workflow (at a minimum, include all required fields) + +This is a good place for us to pause and explain an important distinction -- fields in TAP have two attributes we're interested in: label and name. The field label will appear as "Click to edit" while the field name will be the word 'element' followed by a number appearing below the field. The files here expect to be given field labels, and will convert those into names for you before initiating the workflows. + +In the second row of your CSV, add values for the fields that you specified in the first row + The value for a field should be in the same column/position as the name for that field + Each subsequent row represents a different workflow instance, and you can specify the field values in the same way + +3. Fill in the global variables + +You'll need to ask your ThinkSmart account manager for client id/secret, which are specific to your tenant and environment +TAP shows this information in your browser's address bar: https://{tenant}.{environment}.thinksmart.com/ + +4. Finally, check for best practices + +Check that Form access is configured in your workflow + We can only submit a value to a field set to "Show" + +When entering field names in the first row of your CSV file, watch for extraneous spaces or line breaks + These need to match exactly with the field names in TAP + +If a particular field is unused in all of your workflows, there's no harm in excluding it + +If your field name or value includes commas, wrap it in quotes (text editor, Excel should do this for you) + +All apostrophes and quotes must be straight instead of curly + When these punctuation are curly, they may not be read correctly + +When specifying a user in your workflow, don't use a dropdown with registered users + Instead, use two text fields: one for the name, and another for the Email (with Email validation) + +If choosing an option from a dropdown, use "Edit option values" to set labels and values the same + The code will try to match your input with the values in a dropdown + + +After you've run the bulk_initiate file, a txt file will appear in the same directory as the code. + +The report will include a summary for each row of the CSV. If a workflow was successfully initiated for a particular row, each name-value pair will be listed below the row number. If not, an error message will be shown. + +If there are field names from the workflow that don't match those in the CSV file, or vice-versa, this will be visible at the top of the report. If the workflow contains more than one field with the same name, they will be selected based on position within the form (specifically, the field closest to the top of the form will be filled in first). In addition, the element number (field name) for each will be listed in parentheses next to its label, making it easier to differentiate between duplicates (as field names are unchangeable). + +If you have questions or suggestions, send them to cpierce@thinksmart.com \ No newline at end of file diff --git a/Workflow_via_CSV/bulk_initiate.py b/Workflow_via_CSV/bulk_initiate.py index a35c361..f95dd41 100644 --- a/Workflow_via_CSV/bulk_initiate.py +++ b/Workflow_via_CSV/bulk_initiate.py @@ -27,13 +27,13 @@ # Global Variables # # ---------------- # -# replace tenant, environment, instance with your own -url_root = 'https://default.stagingtap.thinksmart.com/default/' +# replace tenant, environment, and instance with your own +url_root = 'https://tenant.environment.thinksmart.com/instance/' # fill in (must be strings) -client_id = 'b473d2ace41144bb87d63be7584ecec1' -client_secret = 'Yjr1ZnzMgN58x7H+WlPM6WRTzk1B7RocuYkOEuVhcpw=' -workflow_name = 'PyScript Test' -csv_name = '/Users/cameronpierce/Dropbox/thinksmart/api/PyScript.csv' +client_id = '' +client_secret = '' +workflow_name = '' +csv_name = '' # enter username and password in command line during runtime username = input("Username: ") password = getpass.getpass() @@ -68,8 +68,8 @@ if (r.status_code == 404): f.write("Invalid url_root") sys.exit() +# invalid user credentials or client info causes 400 response elif (r.status_code == 400): - # invalid user credentials or client info causes 400 response if (json.loads(r.text).get('error') == 'invalid_grant'): f.write("Invalid Username and/or Password.") elif (json.loads(r.text).get('error') == 'unauthorized_client'): @@ -96,6 +96,7 @@ f.write("Invalid workflow_name. This may occur when {} {}" .format("the name contains non-alphabetic characters or", "none of the workflow fields have been added to the repository.")) + sys.exit() # else assign it else: template_id = temp[0].get('ID')