Skip to content

Commit d1245d9

Browse files
author
Jon Walker
authored
Merge pull request #34 from jameskochubasas/master
Adding workflow capability to sasctl
2 parents 4090d3f + 77bfc9a commit d1245d9

File tree

5 files changed

+282
-2
lines changed

5 files changed

+282
-2
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!coverage.py: This is a private format, don't read it directly!{"lines":{"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/__init__.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/__main__.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/core.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/exceptions.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/services.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/tasks.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/__init__.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/cas_management.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/concepts.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/data_sources.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/files.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/folders.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/microanalytic_score.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/model_management.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/model_publish.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/model_repository.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/projects.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/sentiment_analysis.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/service.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/text_categorization.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/text_parsing.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/_services/workflow.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/utils/__init__.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/utils/astore.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/utils/cli.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/utils/pymas/__init__.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/utils/pymas/core.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/utils/pymas/ds2.py":[],"/home/centos/jamesfork/python-sasctl/.tox/py37-tests/lib/python3.7/site-packages/sasctl/utils/pymas/python.py":[]}}

src/sasctl/_services/model_management.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from .service import Service
88

9-
109
class ModelManagement(Service):
1110
"""The Model Management API provides basic resources for monitoring
1211
performance, comparing models, and running workflow processes.
@@ -232,3 +231,104 @@ def execute_performance_definition(self, definition):
232231

233232
return self.post('/performanceTasks/%s' % definition.id)
234233

234+
def list_model_workflow_definition(self):
235+
"""List all enabled Workflow Processes to execute on Model Project.
236+
237+
Returns
238+
-------
239+
RestObj
240+
The list of workflows
241+
242+
"""
243+
from .workflow import Workflow
244+
wf = Workflow()
245+
246+
return wf.list_workflow_enableddefinitions()
247+
248+
def list_model_workflow_prompt(self, workflowName):
249+
"""List prompt Workflow Processes Definitions.
250+
251+
Parameters
252+
----------
253+
workflowName : str
254+
Name or ID of an enabled workflow to retrieve inputs
255+
256+
Returns
257+
-------
258+
list
259+
The list of prompts for specific workflow
260+
261+
"""
262+
from .workflow import Workflow
263+
wf = Workflow()
264+
265+
return wf.list_workflow_prompt(workflowName)
266+
267+
268+
def list_model_workflow_executed(self, projectName):
269+
"""List prompt Workflow Processes Definitions.
270+
271+
Parameters
272+
----------
273+
projectName : str
274+
Name of the Project list executed workflow
275+
276+
277+
Returns
278+
-------
279+
RestObj
280+
List of workflows associated to project
281+
282+
"""
283+
from .model_repository import ModelRepository
284+
mr = ModelRepository()
285+
286+
project = mr.get_project(projectName)
287+
288+
return self.get('/workflowProcesses?filter=eq(associations.solutionObjectId,%22'+project['id']+'%22)')
289+
290+
291+
def execute_model_workflow_definition(self, projectName, workflowName, input=None):
292+
"""Runs specific Workflow Processes Definitions.
293+
294+
Parameters
295+
----------
296+
projectName : str
297+
Name of the Project that will execute workflow
298+
workflowName : str
299+
Name or ID of an enabled workflow to execute
300+
input : dict, optional
301+
Input values for the workflow for initial workflow prompt
302+
303+
Returns
304+
-------
305+
RestObj
306+
The executing workflow
307+
308+
"""
309+
from .model_repository import ModelRepository
310+
from .workflow import Workflow
311+
mr = ModelRepository()
312+
wf = Workflow()
313+
314+
project = mr.get_project(projectName)
315+
316+
workflow = wf.run_workflow_definition(workflowName, input=input)
317+
318+
#Associations running workflow to model project, note workflow has to be running
319+
# THINK ABOUT: do we do a check on status of the workflow to determine if it is still running before associating?
320+
321+
input={"processName":workflow['name'],"processId":workflow['id'],"objectType":"MM_Project",
322+
"solutionObjectName":projectName,"solutionObjectId":project['id'],
323+
"solutionObjectUri":"/modelRepository/projects/"+project['id'],
324+
"solutionObjectMediaType":"application/vnd.sas.models.project+json"}
325+
326+
#Note, you can get a HTTP Error 404: {"errorCode":74052,"message":"The workflow process for id <> cannot be found.
327+
# Associations can only be made to running processes.","details":["correlator:
328+
# e62c5562-2b11-45db-bcb7-933200cb0f0a","traceId: 3118c0fb1eb9702d","path:
329+
# /modelManagement/workflowAssociations"],"links":[],"version":2,"httpStatusCode":404}
330+
# Which is fine and expected like the Visual Experience.
331+
return self.post('/workflowAssociations', json=input,
332+
headers={'Content-Type': 'application/vnd.sas.workflow.object.association+json'})
333+
334+

src/sasctl/_services/workflow.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env python
2+
# encoding: utf-8
3+
#
4+
# Copyright © 2019, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
from .service import Service
8+
9+
class Workflow(Service):
10+
"""The Workflow API provides basic resources for list, prompt details,
11+
and running workflow processes.
12+
"""
13+
_SERVICE_ROOT= '/workflow'
14+
15+
16+
def list_workflow_enableddefinitions(self):
17+
"""List all enabled Workflow Processes Definitions.
18+
19+
Returns
20+
-------
21+
RestObj
22+
The list of workflows
23+
24+
"""
25+
26+
#Additional header to fix 400 ERROR Bad Request
27+
headers={"Accept-Language": "en-US"}
28+
return self.get('/enabledDefinitions', headers=headers)
29+
30+
def list_workflow_prompt(self, name):
31+
"""List prompt Workflow Processes Definitions.
32+
33+
Parameters
34+
----------
35+
name : str
36+
Name or ID of an enabled workflow to retrieve inputs
37+
38+
Returns
39+
-------
40+
list
41+
The list of prompts for specific workflow
42+
43+
"""
44+
45+
ret = self.__find_specific_workflow(name)
46+
if ret is None:
47+
raise ValueError("No Workflow enabled for %s name or id." % name)
48+
49+
if 'prompts' in ret:
50+
return ret['prompts']
51+
else:
52+
# No prompt inputs on workflow
53+
return None
54+
55+
def run_workflow_definition(self, name, input=None):
56+
"""Runs specific Workflow Processes Definitions.
57+
58+
Parameters
59+
----------
60+
name : str
61+
Name or ID of an enabled workflow to execute
62+
input : dict, optional
63+
Input values for the workflow for initial workflow prompt
64+
65+
Returns
66+
-------
67+
RestObj
68+
The executing workflow
69+
70+
"""
71+
72+
workflow = self.__find_specific_workflow(name)
73+
if workflow is None:
74+
raise ValueError("No Workflow enabled for %s name or id." % name)
75+
76+
if input is None:
77+
return self.post('/processes?definitionId=' + workflow['id'],
78+
headers={'Content-Type': 'application/vnd.sas.workflow.variables+json'})
79+
if isinstance(input, dict):
80+
return self.post('/processes?definitionId=' + workflow['id'], json=input,
81+
headers={'Content-Type': 'application/vnd.sas.workflow.variables+json'})
82+
83+
def __find_specific_workflow(self, name):
84+
# Internal helper method
85+
# Finds a workflow with the name (can be a name or id)
86+
# Returns a dict objects of the workflow
87+
listendef = self.list_workflow_enableddefinitions()
88+
for tmp in listendef:
89+
if tmp['name'] == name:
90+
return tmp
91+
elif tmp['id'] == name:
92+
return tmp
93+
94+
# Did not find any enabled workflow with name/id
95+
return None
96+
97+

tests/unit/test_model_management.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,20 @@ def test_table_prefix_format():
8282
# Underscores should not be allowed
8383
_ = mm.create_performance_definition('model',
8484
'TestLibrary',
85-
'invalid_name')
85+
'invalid_name')
86+
87+
def test_execute_model_workflow_definition_invalidworkflow():
88+
89+
PROJECT = RestObj({'name': 'Test Project', 'id': '98765'})
90+
WORKFLOWS = [{'name': 'Test W', 'id': '12345'},{'name': 'TestW2', 'id': '98765', 'prompts': [{'id': '98765', 'variableName': 'projectId', 'variableType': 'string'}]}]
91+
92+
with mock.patch('sasctl._services.workflow.Workflow'
93+
'.list_workflow_enableddefinitions') as list_workflow_enableddefinitions:
94+
with mock.patch('sasctl._services.model_repository.ModelRepository'
95+
'.get_project') as get_project:
96+
list_workflow_enableddefinitions.return_value = WORKFLOWS
97+
get_project.return_value = PROJECT
98+
with pytest.raises(ValueError):
99+
# Project missing
100+
_ = mm.execute_model_workflow_definition('TestLibrary','badworkflow')
101+

tests/unit/test_workflow.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env python
2+
# encoding: utf-8
3+
#
4+
# Copyright © 2019, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
import pytest
8+
from six.moves import mock
9+
10+
from sasctl import RestObj
11+
from sasctl._services import workflow
12+
13+
14+
def test_list_workflow_prompt_invalidworkflow():
15+
16+
WORKFLOWS = [{'name': 'Test W', 'id': '12345'},{'name': 'TestW2', 'id': '98765', 'prompts': [{'id': '98765', 'variableName': 'projectId', 'variableType': 'string'}]}]
17+
wf = workflow.Workflow()
18+
19+
with mock.patch('sasctl._services.workflow.Workflow'
20+
'.list_workflow_enableddefinitions') as list_workflow_enableddefinitions:
21+
list_workflow_enableddefinitions.return_value = WORKFLOWS
22+
with pytest.raises(ValueError):
23+
# Project missing
24+
_ = wf.list_workflow_prompt('bad')
25+
26+
def test_list_workflow_prompt_workflownoprompt():
27+
28+
WORKFLOWS = [{'name': 'Test W', 'id': '12345'},{'name': 'TestW2', 'id': '98765', 'prompts': [{'id': '98765', 'variableName': 'projectId', 'variableType': 'string'}]}]
29+
wf = workflow.Workflow()
30+
31+
with mock.patch('sasctl._services.workflow.Workflow'
32+
'.list_workflow_enableddefinitions') as list_workflow_enableddefinitions:
33+
list_workflow_enableddefinitions.return_value = WORKFLOWS
34+
35+
#Testing no prompts on workflow with name
36+
testresult = wf.list_workflow_prompt('Test W')
37+
print(testresult)
38+
assert testresult is None
39+
40+
#Testing no prompts on workflow with id
41+
testresult = wf.list_workflow_prompt('12345')
42+
print(testresult)
43+
assert testresult is None
44+
45+
46+
47+
def test_list_workflow_prompt_workflowprompt():
48+
49+
WORKFLOWS = [{'name': 'Test W', 'id': '12345'},{'name': 'TestW2', 'id': '98765', 'prompts': [{'id': '98765', 'variableName': 'projectId', 'variableType': 'string'}]}]
50+
wf = workflow.Workflow()
51+
52+
with mock.patch('sasctl._services.workflow.Workflow'
53+
'.list_workflow_enableddefinitions') as list_workflow_enableddefinitions:
54+
list_workflow_enableddefinitions.return_value = WORKFLOWS
55+
56+
#Testing workflow with id and prompts
57+
testresult = wf.list_workflow_prompt('TestW2')
58+
print(testresult)
59+
assert testresult is not None
60+
61+
62+
#Testing workflow with id and prompts
63+
testresult = wf.list_workflow_prompt('98765')
64+
print(testresult)
65+
assert testresult is not None
66+

0 commit comments

Comments
 (0)