Moving to YAQL 1.0

Implements blueprint yaql-v1-0

 * Now functions 'env()' and 'execution()' should
 be used instead of '$.__env' and '$.__execution'
 correspondingly.

TODO:
 - Update examples in mistral-extra

Change-Id: Ic96fe10966bf5d27898c08d70914eb294b7efe2f
This commit is contained in:
Nikolay Mahotkin 2015-07-28 15:58:00 +03:00
parent 0eb4fdad31
commit 6b89f6918e
11 changed files with 91 additions and 83 deletions

View File

@ -20,14 +20,15 @@ import re
from oslo_log import log as logging from oslo_log import log as logging
import six import six
import yaql from yaql.language import exceptions as yaql_exc
from yaql import exceptions as yaql_exc from yaql.language import factory
from mistral import exceptions as exc from mistral import exceptions as exc
from mistral import yaql_utils from mistral.utils import yaql_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
YAQL_ENGINE = factory.YaqlFactory().create()
class Evaluator(object): class Evaluator(object):
@ -75,7 +76,7 @@ class YAQLEvaluator(Evaluator):
LOG.debug("Validating YAQL expression [expression='%s']", expression) LOG.debug("Validating YAQL expression [expression='%s']", expression)
try: try:
yaql.parse(expression) YAQL_ENGINE(expression)
except (yaql_exc.YaqlException, KeyError, ValueError, TypeError) as e: except (yaql_exc.YaqlException, KeyError, ValueError, TypeError) as e:
raise exc.YaqlEvaluationException(e.message) raise exc.YaqlEvaluationException(e.message)
@ -85,9 +86,8 @@ class YAQLEvaluator(Evaluator):
% (expression, data_context)) % (expression, data_context))
try: try:
result = yaql.parse(expression).evaluate( result = YAQL_ENGINE(expression).evaluate(
data=data_context, context=yaql_utils.get_yaql_context(data_context)
context=yaql_utils.create_yaql_context()
) )
except (yaql_exc.YaqlException, KeyError, ValueError, TypeError) as e: except (yaql_exc.YaqlException, KeyError, ValueError, TypeError) as e:
raise exc.YaqlEvaluationException( raise exc.YaqlEvaluationException(

View File

@ -36,7 +36,7 @@ VARIABLES = {
'verbose': True, 'verbose': True,
'__actions': { '__actions': {
'std.sql': { 'std.sql': {
'conn': 'mysql://admin:secrete@{$.__env.host}/{$.__env.db}' 'conn': 'mysql://admin:secrete@<% env().host %>/<% env().db %>'
} }
} }
} }

View File

@ -56,7 +56,7 @@ class DataFlowEngineTest(engine_test_base.EngineTestCase):
task3: task3:
publish: publish:
result: "<% $.hi %>, <% $.to %>! Your <% $.__env.from %>." result: "<% $.hi %>, <% $.to %>! Your <% env().from %>."
""" """
wf_service.create_workflows(linear_wf) wf_service.create_workflows(linear_wf)
@ -113,7 +113,7 @@ class DataFlowEngineTest(engine_test_base.EngineTestCase):
task3: task3:
publish: publish:
result: "<% $.hi %>, <% $.to %>! Your <% $.__env.from %>." result: "<% $.hi %>, <% $.to %>! Your <% env().from %>."
progress: "completed task3" progress: "completed task3"
on-success: on-success:
- notify - notify
@ -338,7 +338,7 @@ class DataFlowEngineTest(engine_test_base.EngineTestCase):
task4: task4:
publish: publish:
result: "<% $.greeting %>, <% $.to %>! <% $.__env.from %>." result: "<% $.greeting %>, <% $.to %>! <% env().from %>."
""" """
wf_service.create_workflows(var_overwrite_wf) wf_service.create_workflows(var_overwrite_wf)

View File

@ -194,8 +194,8 @@ class DefaultEngineTest(base.DbTestCase):
def test_start_workflow_with_adhoc_env(self): def test_start_workflow_with_adhoc_env(self):
wf_input = { wf_input = {
'param1': '<% $.__env.key1 %>', 'param1': '<% env().key1 %>',
'param2': '<% $.__env.key2 %>' 'param2': '<% env().key2 %>'
} }
env = ENVIRONMENT['variables'] env = ENVIRONMENT['variables']
@ -215,8 +215,8 @@ class DefaultEngineTest(base.DbTestCase):
@mock.patch.object(db_api, "get_environment", MOCK_ENVIRONMENT) @mock.patch.object(db_api, "get_environment", MOCK_ENVIRONMENT)
def test_start_workflow_with_saved_env(self): def test_start_workflow_with_saved_env(self):
wf_input = { wf_input = {
'param1': '<% $.__env.key1 %>', 'param1': '<% env().key1 %>',
'param2': '<% $.__env.key2 %>' 'param2': '<% env().key2 %>'
} }
env = ENVIRONMENT['variables'] env = ENVIRONMENT['variables']
@ -238,7 +238,7 @@ class DefaultEngineTest(base.DbTestCase):
self.assertRaises(exc.NotFoundException, self.assertRaises(exc.NotFoundException,
self.engine.start_workflow, self.engine.start_workflow,
'wb.wf', 'wb.wf',
{'param1': '<% $.__env.key1 %>'}, {'param1': '<% env().key1 %>'},
env='foo', env='foo',
task_name='task2') task_name='task2')
@ -246,7 +246,7 @@ class DefaultEngineTest(base.DbTestCase):
self.assertRaises(ValueError, self.assertRaises(ValueError,
self.engine.start_workflow, self.engine.start_workflow,
'wb.wf', 'wb.wf',
{'param1': '<% $.__env.key1 %>'}, {'param1': '<% env().key1 %>'},
env=True, env=True,
task_name='task2') task_name='task2')

View File

@ -57,7 +57,7 @@ workflows:
type: direct type: direct
tasks: tasks:
t1: t1:
with-items: i in <% range(0, 3).list() %> with-items: i in <% list(range(0, 3)) %>
action: std.echo output="Task 1.<% $.i %>" action: std.echo output="Task 1.<% $.i %>"
publish: publish:
v1: <% $.t1 %> v1: <% $.t1 %>

View File

@ -51,14 +51,14 @@ workflows:
tasks: tasks:
task1: task1:
action: std.echo output=<% $.param1 %> action: std.echo output=<% $.param1 %>
target: <% $.__env.var1 %> target: <% env().var1 %>
publish: publish:
result1: <% $.task1 %> result1: <% $.task1 %>
task2: task2:
requires: [task1] requires: [task1]
action: std.echo output="'<% $.result1 %> & <% $.param2 %>'" action: std.echo output="'<% $.result1 %> & <% $.param2 %>'"
target: <% $.__env.var1 %> target: <% env().var1 %>
publish: publish:
final_result: <% $.task2 %> final_result: <% $.task2 %>
@ -70,11 +70,11 @@ workflows:
task1: task1:
workflow: wf1 workflow: wf1
input: input:
param1: <% $.__env.var2 %> param1: <% env().var2 %>
param2: <% $.__env.var3 %> param2: <% env().var3 %>
task_name: task2 task_name: task2
publish: publish:
slogan: "<% $.task1.final_result %> is a cool <% $.__env.var4 %>!" slogan: "<% $.task1.final_result %> is a cool <% env().var4 %>!"
""" """
@ -191,7 +191,7 @@ class SubworkflowsTest(base.EngineTestCase):
env = { env = {
'var1': TARGET, 'var1': TARGET,
'var2': 'Bonnie', 'var2': 'Bonnie',
'var3': '<% $.__env.var5 %>', 'var3': '<% env().var5 %>',
'var4': 'movie', 'var4': 'movie',
'var5': 'Clyde' 'var5': 'Clyde'
} }

View File

@ -319,8 +319,8 @@ class JoinEngineTest(base.EngineTestCase):
action: std.echo action: std.echo
input: input:
output: | output: |
<% result1 in $ %>,<% result2 in $ %>, <% result1 in $.keys() %>,<% result2 in $.keys() %>,
<% result3 in $ %>,<% result4 in $ %> <% result3 in $.keys() %>,<% result4 in $.keys() %>
publish: publish:
result5: <% $.task5 %> result5: <% $.task5 %>
""" """
@ -390,8 +390,8 @@ class JoinEngineTest(base.EngineTestCase):
action: std.echo action: std.echo
input: input:
output: | output: |
<% result1 in $ %>,<% result2 in $ %>, <% result1 in $.keys() %>,<% result2 in $.keys() %>,
<% result3 in $ %> <% result3 in $.keys() %>
publish: publish:
result4: <% $.task4 %> result4: <% $.task4 %>
""" """

View File

@ -59,7 +59,12 @@ class YaqlEvaluatorTest(base.BaseTest):
res = self._evaluator.evaluate("$.status = 'Invalid value'", DATA) res = self._evaluator.evaluate("$.status = 'Invalid value'", DATA)
self.assertFalse(res) self.assertFalse(res)
self.assertIsNone(self._evaluator.evaluate('$.wrong_key', DATA)) self.assertRaises(
exc.YaqlEvaluationException,
self._evaluator.evaluate,
'$.wrong_key',
DATA
)
expression_str = 'invalid_expression_string' expression_str = 'invalid_expression_string'
res = self._evaluator.evaluate(expression_str, DATA) res = self._evaluator.evaluate(expression_str, DATA)
@ -78,12 +83,12 @@ class YaqlEvaluatorTest(base.BaseTest):
self.assertEqual('3', self._evaluator.evaluate('str($)', 3)) self.assertEqual('3', self._evaluator.evaluate('str($)', 3))
def test_function_len(self): def test_function_len(self):
self.assertEqual(3, self._evaluator.evaluate('$.len()', 'hey')) self.assertEqual(3, self._evaluator.evaluate('len($)', 'hey'))
data = [{'some': 'thing'}] data = [{'some': 'thing'}]
self.assertEqual( self.assertEqual(
1, 1,
self._evaluator.evaluate('$[$.some = thing].len()', data) self._evaluator.evaluate('$.where($.some = thing).len()', data)
) )
def test_validate(self): def test_validate(self):
@ -289,8 +294,8 @@ class ExpressionsTest(base.BaseTest):
'verbose': True, 'verbose': True,
'__actions': { '__actions': {
'std.sql': { 'std.sql': {
'conn': 'mysql://admin:secrete@<% $.__env.host %>' 'conn': 'mysql://admin:secrete@<% env().host %>'
'/<% $.__env.db %>' '/<% env().db %>'
} }
} }
} }

View File

@ -0,0 +1,52 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import yaql
ROOT_CONTEXT = None
def get_yaql_context(data_context):
global ROOT_CONTEXT
if not ROOT_CONTEXT:
ROOT_CONTEXT = yaql.create_context()
_register_functions(ROOT_CONTEXT)
new_ctx = ROOT_CONTEXT.create_child_context()
new_ctx['$'] = data_context
if isinstance(data_context, dict):
new_ctx['__env'] = data_context.get('__env')
new_ctx['__execution'] = data_context.get('__execution')
return new_ctx
def _register_functions(yaql_ctx):
yaql_ctx.register_function(env_)
yaql_ctx.register_function(execution_)
# Additional convenience YAQL functions.
# If a function name ends with underscore then it doesn't need to pass
# the name of the function when context registers it.
def env_(context):
return context['__env']
def execution_(context):
return context['__execution']

View File

@ -1,49 +0,0 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import yaql
from yaql import context
def create_yaql_context():
ctx = yaql.create_context()
_register_functions(ctx)
return ctx
def _register_functions(yaql_ctx):
yaql_ctx.register_function(_sized_length, 'len')
yaql_ctx.register_function(_iterable_length, 'len')
yaql_ctx.register_function(to_str, 'str')
# Additional convenience YAQL functions.
@context.EvalArg('a', arg_type=collections.Sized)
def _sized_length(a):
return len(a)
@context.EvalArg('a', arg_type=collections.Iterable)
def _iterable_length(a):
return sum(1 for i in a)
def to_str(value):
return str(value())

View File

@ -37,5 +37,5 @@ six>=1.9.0
SQLAlchemy<1.1.0,>=0.9.7 SQLAlchemy<1.1.0,>=0.9.7
stevedore>=1.5.0 # Apache-2.0 stevedore>=1.5.0 # Apache-2.0
WSME>=0.7 WSME>=0.7
yaql>=0.2.7,!=0.3.0 # Apache 2.0 License yaql>=1.0.0 # Apache 2.0 License
tooz>=0.16.0 # Apache-2.0 tooz>=0.16.0 # Apache-2.0