Add composer.task
diff --git a/src/composer/__init__.py b/src/composer/__init__.py
index bcab68e..93d7c10 100644
--- a/src/composer/__init__.py
+++ b/src/composer/__init__.py
@@ -14,6 +14,9 @@
def literal(value):
return _composer.literal(value)
+def task(value):
+ return _composer.task(value)
+
def function(value):
return _composer.function(value)
diff --git a/src/composer/composer.py b/src/composer/composer.py
index abc18af..5cfc94c 100644
--- a/src/composer/composer.py
+++ b/src/composer/composer.py
@@ -77,12 +77,12 @@
setattr(self, arg['_'], f(getattr(self, arg['_']), arg['_']))
class Compiler:
- def literal(self, value):
- return self._compose('literal', (value,))
-
def empty(self):
return self._compose('empty', ())
+ def literal(self, value):
+ return self._compose('literal', (value,))
+
def seq(self, *arguments):
return self._compose('seq', arguments)
@@ -100,7 +100,8 @@
if isinstance(task, Composition):
return task
- # if (typeof task === 'function') return this.function(task)
+ if callable(task):
+ return self.function(task)
if isinstance(task, str): # python3 only
return self.action(task)
@@ -214,15 +215,39 @@
if 'actions' in obj:
for action in obj['actions']:
+ print('ACTION:', action)
action['serializer'] = serialize
self.actions.delete(action)
self.actions.update(action)
class Composer(Compiler):
+ def action(self, name, options = {}):
+ ''' enhanced action combinator: mangle name, capture code '''
+ name = parse_action_name(name) # throws ComposerError if name is not valid
+ exec = None
+ if hasattr(options, 'sequence'): # native sequence
+ exec = { 'kind': 'sequence', 'components': tuple(map(parse_action_name, options['sequence'])) }
- # return enhanced openwhisk client capable of deploying compositions
+ if hasattr(options, 'filename') and isinstance(options['filename'], str): # read action code from file
+ raise ComposerError('read from file not implemented')
+ # exec = fs.readFileSync(options.filename, { encoding: 'utf8' })
+
+ # if (typeof options.action === 'function') { // capture function
+ # exec = `const main = ${options.action}`
+ # if (exec.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function', options.action)
+ # }
+
+ if hasattr(options, 'action') and (isinstance(options['action'], str) or isinstance(options['action'], dict)):
+ exec = options['action']
+
+ if isinstance(exec, str):
+ exec = { 'kind': 'nodejs:default', 'code': exec }
+
+ return Composition(type='action', exec=exec, name=name)
+
def openwhisk(self, options):
- ''' try to extract apihost and key first from whisk property file file and then from os.environ '''
+ ''' return enhanced openwhisk client capable of deploying compositions '''
+ # try to extract apihost and key first from whisk property file file and then from os.environ
wskpropsPath = os.environ['WSK_CONFIG_FILE'] if 'WSK_CONFIG_FILE' in os.environ else os.path.expanduser('~/.wskprops')
with open(wskpropsPath) as f:
diff --git a/src/openwhisk/openwhisk.py b/src/openwhisk/openwhisk.py
index 4c974bd..5c57afd 100644
--- a/src/openwhisk/openwhisk.py
+++ b/src/openwhisk/openwhisk.py
@@ -56,7 +56,7 @@
serializer = options['serializer'] if 'serializer' in options else None
payload = json.dumps(body, default=serializer)
-
+ print('\n\nPAYLOAD\n\n:', payload)
headers = { 'Authorization': self.auth_header(), 'Content-Type': 'application/json' }
verify = not self.options['ignore_certs']
@@ -66,7 +66,8 @@
# we turn >=400 statusCode responses into exceptions
error = Exception()
error.status_code = resp.status_code
- error.error = resp.text
+ error.error = resp.reason
+ print('ERROR', resp.reason)
raise error
else:
# otherwise, the response body is the expected return value
@@ -230,10 +231,10 @@
if 'action' not in options:
raise Exception(missing_action_body_error)
- body = { 'exec': { 'kind': options['kind'] if 'kind' in options else 'nodejs:default', 'code': options['action'] } }
+ body = { 'exec': { 'kind': options['kind'] if 'kind' in options else 'python:default', 'code': options['action'] } }
if isinstance(options['action'], bytes):
- body['exec']['code'] =base64.encodebytes(options['action'])
+ body['exec']['code'] = base64.encodebytes(options['action'])
elif isinstance(options['action'], dict):
return options['action']
diff --git a/tests/test_composer.py b/tests/test_composer.py
index bb67322..8d1cb34 100644
--- a/tests/test_composer.py
+++ b/tests/test_composer.py
@@ -9,7 +9,10 @@
def define(action):
''' deploy action '''
- wsk.actions.delete(action['name'])
+ try:
+ wsk.actions.delete(action['name'])
+ except:
+ pass
wsk.actions.create(action)
def invoke(task, params = {}, blocking = True):
@@ -17,35 +20,43 @@
wsk.compositions.deploy(composer.composition(name, task))
return wsk.actions.invoke({ 'name': name, 'params': params, 'blocking': blocking })
+@pytest.fixture(scope="session", autouse=True)
+def deploy_actions():
+ define({ 'name': 'echo', 'action': 'const main = x=>x', 'kind': 'nodejs:default' })
+ define({ 'name': 'DivideByTwo', 'action': 'function main({n}) { return { n: n / 2 } }', 'kind': 'nodejs:default'})
+ define({ 'name': 'TripleAndIncrement', 'action': 'function main({n}) { return { n: n * 3 + 1 } }', 'kind': 'nodejs:default' })
+ define({ 'name': 'isNotOne', 'action': 'function main({n}) { return { value: n != 1 } }', 'kind': 'nodejs:default' })
+ define({ 'name': 'isEven', 'action': 'function main({n}) { return { value: n % 2 == 0 } }', 'kind': 'nodejs:default'})
-def test_parse_action_name():
- combos = [
- { "n": "", "s": False, "e": "Name is not specified" },
- { "n": " ", "s": False, "e": "Name is not specified" },
- { "n": "/", "s": False, "e": "Name is not valid" },
- { "n": "//", "s": False, "e": "Name is not valid" },
- { "n": "/a", "s": False, "e": "Name is not valid" },
- { "n": "/a/b/c/d", "s": False, "e": "Name is not valid" },
- { "n": "/a/b/c/d/", "s": False, "e": "Name is not valid" },
- { "n": "a/b/c/d", "s": False, "e": "Name is not valid" },
- { "n": "/a/ /b", "s": False, "e": "Name is not valid" },
- { "n": "a", "e": False, "s": "/_/a" },
- { "n": "a/b", "e": False, "s": "/_/a/b" },
- { "n": "a/b/c", "e": False, "s": "/a/b/c" },
- { "n": "/a/b", "e": False, "s": "/a/b" },
- { "n": "/a/b/c", "e": False, "s": "/a/b/c" }
- ]
- for combo in combos:
- if combo["s"] is not False:
- # good cases
- assert composer.parse_action_name(combo["n"]) == combo["s"]
- else:
- # error cases
- try:
- composer.parse_action_name(combo["n"])
- assert False
- except composer.ComposerError as error:
- assert error.message == combo["e"]
+class TestAction:
+ def test_parse_action_name(self):
+ combos = [
+ { "n": "", "s": False, "e": "Name is not specified" },
+ { "n": " ", "s": False, "e": "Name is not specified" },
+ { "n": "/", "s": False, "e": "Name is not valid" },
+ { "n": "//", "s": False, "e": "Name is not valid" },
+ { "n": "/a", "s": False, "e": "Name is not valid" },
+ { "n": "/a/b/c/d", "s": False, "e": "Name is not valid" },
+ { "n": "/a/b/c/d/", "s": False, "e": "Name is not valid" },
+ { "n": "a/b/c/d", "s": False, "e": "Name is not valid" },
+ { "n": "/a/ /b", "s": False, "e": "Name is not valid" },
+ { "n": "a", "e": False, "s": "/_/a" },
+ { "n": "a/b", "e": False, "s": "/_/a/b" },
+ { "n": "a/b/c", "e": False, "s": "/a/b/c" },
+ { "n": "/a/b", "e": False, "s": "/a/b" },
+ { "n": "/a/b/c", "e": False, "s": "/a/b/c" }
+ ]
+ for combo in combos:
+ if combo["s"] is not False:
+ # good cases
+ assert composer.parse_action_name(combo["n"]) == combo["s"]
+ else:
+ # error cases
+ try:
+ composer.parse_action_name(combo["n"])
+ assert False
+ except composer.ComposerError as error:
+ assert error.message == combo["e"]
@pytest.mark.literal
class TestLiteral:
@@ -73,3 +84,26 @@
activation = invoke(composer.function(lambda args: args['n'] % 2 == 0), { 'n': 4 })
assert activation['response']['result'] == { 'value': True }
+class TestTasks:
+
+ def test_task_action(self):
+ activation = invoke(composer.task('isNotOne'), { 'n': 0 })
+ assert activation['response']['result'] == { 'value': True }
+
+ @pytest.mark.skip(reason='need python conductor')
+ def test_task_function(self):
+ activation = invoke(composer.task(lambda args: args['n'] % 2 == 0), { 'n': 4 })
+ assert activation['response']['result'] == { 'value': True }
+
+ def test_task_none(self):
+ activation = invoke(composer.task(None), { 'foo': 'foo' })
+ assert activation['response']['result'] == { 'foo': 'foo' }
+
+ def test_task_fail(self):
+ '''none task must fail on error input'''
+ try:
+ invoke(composer.task(None), { 'error': 'foo' })
+ assert False
+ except Exception as error:
+ print(error)
+