From 6bef102fd40a9bbfb42af9b0d6e70c3e740e0c65 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 26 Sep 2017 22:10:53 -0400 Subject: [PATCH 01/12] Initial work on some additional JSON Jinja filters --- fixed-requirements.txt | 1 + requirements.txt | 1 + st2common/in-requirements.txt | 1 + st2common/st2common/jinja/filters/data.py | 10 +++ .../st2common/jinja/filters/jmespath_query.py | 43 ++++++++++++ st2common/st2common/util/jinja.py | 7 +- .../unit/test_jinja_render_data_filters.py | 24 +++++++ ...est_jinja_render_jmespath_query_filters.py | 66 +++++++++++++++++++ 8 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 st2common/st2common/jinja/filters/jmespath_query.py create mode 100644 st2common/tests/unit/test_jinja_render_jmespath_query_filters.py diff --git a/fixed-requirements.txt b/fixed-requirements.txt index 20c8cc5c02..1ef7866a41 100644 --- a/fixed-requirements.txt +++ b/fixed-requirements.txt @@ -15,6 +15,7 @@ pyyaml>=3.12,<4.0 requests[security]>=2.14.1,<2.15 apscheduler==3.3.1 gitpython==2.1.5 +jmespath<1.0.0,>=0.9.3 jsonschema==2.6.0 # Note: mongoengine v0.13.0 introduces memory usage regression so we can't # upgrade - https://github.com/StackStorm/st2/pull/3597 diff --git a/requirements.txt b/requirements.txt index 917a17908b..aa7132fdb8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ gitpython==2.1.5 gunicorn==19.7.1 ipaddr jinja2 +jmespath<1.0.0,>=0.9.3 jsonpath-rw==1.4.0 jsonschema==2.6.0 kombu==4.0.2 diff --git a/st2common/in-requirements.txt b/st2common/in-requirements.txt index 149062478a..617cbef1ee 100644 --- a/st2common/in-requirements.txt +++ b/st2common/in-requirements.txt @@ -3,6 +3,7 @@ apscheduler python-dateutil eventlet jinja2 +jmespath jsonschema kombu mongoengine diff --git a/st2common/st2common/jinja/filters/data.py b/st2common/st2common/jinja/filters/data.py index b19d61244e..a65ab9609f 100644 --- a/st2common/st2common/jinja/filters/data.py +++ b/st2common/st2common/jinja/filters/data.py @@ -17,11 +17,21 @@ import yaml __all__ = [ + 'from_json_string', + 'from_yaml_string', 'to_json_string', 'to_yaml_string', ] +def from_json_string(value): + return json.loads(value) + + +def from_yaml_string(value): + return yaml.safe_load(value) + + def to_json_string(value, indent=4, sort_keys=False, separators=(',', ':')): return json.dumps(value, indent=indent, separators=separators, sort_keys=sort_keys) diff --git a/st2common/st2common/jinja/filters/jmespath_query.py b/st2common/st2common/jinja/filters/jmespath_query.py new file mode 100644 index 0000000000..277f4721f8 --- /dev/null +++ b/st2common/st2common/jinja/filters/jmespath_query.py @@ -0,0 +1,43 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 jmespath + +__all__ = [ + 'jmespath_query', + 'jmespath_query_str', +] + + +def jmespath_query(value, query): + """Extracts data from an object `value` using a jmespath `query`. + :link: http://jmespath.org + :param value: a object (dict, array, etc) to query + :param query: a jmsepath query expression (string) + :returns: the result of the query executed on the value + :rtype: dict, array, int, string, bool + """ + return jmespath.search(query, value) + +def jmespath_query_str(value, query): + """Extracts data from a JSON string `value` using a jmespath `query`. + :link: http://jmespath.org + :param value: a json string to query + :param query: a jmsepath query expression (string) + :returns: the result of the query executed on the value + :rtype: dict, array, int, string, bool + """ + obj = json.loads(value) + return jmespath.search(query, obj) diff --git a/st2common/st2common/util/jinja.py b/st2common/st2common/util/jinja.py index 8b9f679494..c88336f8c5 100644 --- a/st2common/st2common/util/jinja.py +++ b/st2common/st2common/util/jinja.py @@ -60,12 +60,15 @@ def get_filters(): from st2common.jinja.filters import time from st2common.jinja.filters import version from st2common.jinja.filters import json_escape + from st2common.jinja.filters import jmespath_query # IMPORTANT NOTE - these filters were recently duplicated in st2mistral so that # they are also available in Mistral workflows. Please ensure any additions you # make here are also made there so that feature parity is maintained. return { 'decrypt_kv': crypto.decrypt_kv, + 'from_json_string': data.from_json_string, + 'from_yaml_string': data.from_yaml_string, 'to_json_string': data.to_json_string, 'to_yaml_string': data.to_yaml_string, @@ -89,7 +92,9 @@ def get_filters(): 'version_strip_patch': version.version_strip_patch, 'use_none': use_none, - 'json_escape': json_escape.json_escape + 'json_escape': json_escape.json_escape, + 'jmespath_query': jmespath_query.jmespath_query, + 'jmespath_query_str': jmespath_query.jmespath_query_str } diff --git a/st2common/tests/unit/test_jinja_render_data_filters.py b/st2common/tests/unit/test_jinja_render_data_filters.py index 2fe349e5ae..28c4251b56 100644 --- a/st2common/tests/unit/test_jinja_render_data_filters.py +++ b/st2common/tests/unit/test_jinja_render_data_filters.py @@ -22,6 +22,30 @@ class JinjaUtilsDataFilterTestCase(unittest2.TestCase): + def test_filter_from_json_string(self): + env = jinja_utils.get_jinja_environment() + expected_obj = {'a': 'b', 'c': {'d': 'e', 'f': 1, 'g': True}} + obj_json_str = '{"a": "b", "c": {"d": "e", "f": 1, "g": true}}' + + template = '{{k1 | from_json_string}}' + + obj = env.from_string(template).render({'k1': obj}) + self.assertDictEqual(obj, expected_obj) + + def test_filter_from_yaml_string(self): + env = jinja_utils.get_jinja_environment() + expected_obj = {'a': 'b', 'c': {'d': 'e', 'f': 1, 'g': True}} + obj_yaml_str = ("---\n" + "a: b\n" + "c:\n" + " d: e\n" + " f: 1\n" + " g: true\n") + + template = '{{k1 | from_yaml_string}}' + obj = env.from_string(template).render({'k1': obj}) + self.assertDictEqual(obj, expected_obj) + def test_filter_to_json_string(self): env = jinja_utils.get_jinja_environment() obj = {'a': 'b', 'c': {'d': 'e', 'f': 1, 'g': True}} diff --git a/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py b/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py new file mode 100644 index 0000000000..58cc5c19d9 --- /dev/null +++ b/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py @@ -0,0 +1,66 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 unittest2 + +from st2common.util import jinja as jinja_utils + + +class JinjaUtilsJmespathQueryTestCase(unittest2.TestCase): + + def test_jmespath_query_static(self): + env = jinja_utils.get_jinja_environment() + obj = {'people': [{'first': 'James', 'last': 'd'}, + {'first': 'Jacob', 'last': 'e'}, + {'first': 'Jayden', 'last': 'f'}, + {'missing': 'different'}], + 'foo': {'bar': 'baz'}} + + template = '{{ obj | jmespath_query("people[*].first") }}' + actual = env.from_string(template).render({'obj': obj}) + expected = ['James', 'Jacob', 'Jayden'] + self.assertEqual(actual, expected) + + + def test_jmespath_query_dynamic(self): + env = jinja_utils.get_jinja_environment() + obj = {'people': [{'first': 'James', 'last': 'd'}, + {'first': 'Jacob', 'last': 'e'}, + {'first': 'Jayden', 'last': 'f'}, + {'missing': 'different'}], + 'foo': {'bar': 'baz'}} + query = "people[*].last" + + template = '{{ obj | jmespath_query(query) }}' + actual = env.from_string(template).render({'obj': obj, + 'query': query}) + expected = ['d', 'e', 'f'] + self.assertEqual(actual, expected) + + + def test_jmespath_query_str(self): + env = jinja_utils.get_jinja_environment() + obj = {'people': [{'first': 'James', 'last': 'd'}, + {'first': 'Jacob', 'last': 'e'}, + {'first': 'Jayden', 'last': 'f'}, + {'missing': 'different'}], + 'foo': {'bar': 'baz'}} + obj_json_str = json.dumps(obj) + + template = '{{ obj_str | jmespath_query_str("people[*].first") }}' + actual = env.from_string(template).render({'obj_str': obj}) + expected = ['James', 'Jacob', 'Jayden'] + self.assertEqual(actual, expected) From dd7894a85d36bee486490c1f7c8ed35abf96537c Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 26 Sep 2017 22:24:09 -0400 Subject: [PATCH 02/12] Fix unit tests for new JSON jinja filters --- st2common/st2common/jinja/filters/jmespath_query.py | 1 + .../tests/unit/test_jinja_render_data_filters.py | 6 ++++-- .../unit/test_jinja_render_jmespath_query_filters.py | 12 ++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/st2common/st2common/jinja/filters/jmespath_query.py b/st2common/st2common/jinja/filters/jmespath_query.py index 277f4721f8..c8142bc9be 100644 --- a/st2common/st2common/jinja/filters/jmespath_query.py +++ b/st2common/st2common/jinja/filters/jmespath_query.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import jmespath __all__ = [ diff --git a/st2common/tests/unit/test_jinja_render_data_filters.py b/st2common/tests/unit/test_jinja_render_data_filters.py index 28c4251b56..87b253bf68 100644 --- a/st2common/tests/unit/test_jinja_render_data_filters.py +++ b/st2common/tests/unit/test_jinja_render_data_filters.py @@ -29,7 +29,8 @@ def test_filter_from_json_string(self): template = '{{k1 | from_json_string}}' - obj = env.from_string(template).render({'k1': obj}) + obj_str = env.from_string(template).render({'k1': obj_json_str}) + obj = eval(obj_str) self.assertDictEqual(obj, expected_obj) def test_filter_from_yaml_string(self): @@ -43,7 +44,8 @@ def test_filter_from_yaml_string(self): " g: true\n") template = '{{k1 | from_yaml_string}}' - obj = env.from_string(template).render({'k1': obj}) + obj_str = env.from_string(template).render({'k1': obj_yaml_str}) + obj = eval(obj_str) self.assertDictEqual(obj, expected_obj) def test_filter_to_json_string(self): diff --git a/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py b/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py index 58cc5c19d9..87aeff7328 100644 --- a/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py +++ b/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py @@ -14,6 +14,7 @@ # limitations under the License. +import json import unittest2 from st2common.util import jinja as jinja_utils @@ -30,7 +31,8 @@ def test_jmespath_query_static(self): 'foo': {'bar': 'baz'}} template = '{{ obj | jmespath_query("people[*].first") }}' - actual = env.from_string(template).render({'obj': obj}) + actual_str = env.from_string(template).render({'obj': obj}) + actual = eval(actual_str) expected = ['James', 'Jacob', 'Jayden'] self.assertEqual(actual, expected) @@ -45,8 +47,9 @@ def test_jmespath_query_dynamic(self): query = "people[*].last" template = '{{ obj | jmespath_query(query) }}' - actual = env.from_string(template).render({'obj': obj, - 'query': query}) + actual_str = env.from_string(template).render({'obj': obj, + 'query': query}) + actual = eval(actual_str) expected = ['d', 'e', 'f'] self.assertEqual(actual, expected) @@ -61,6 +64,7 @@ def test_jmespath_query_str(self): obj_json_str = json.dumps(obj) template = '{{ obj_str | jmespath_query_str("people[*].first") }}' - actual = env.from_string(template).render({'obj_str': obj}) + actual_str = env.from_string(template).render({'obj_str': obj_json_str}) + actual = eval(actual_str) expected = ['James', 'Jacob', 'Jayden'] self.assertEqual(actual, expected) From 8f2527679948f012cc0c8db90a67db50ff9c91c7 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 26 Sep 2017 22:41:13 -0400 Subject: [PATCH 03/12] Added unit and integration tests for new JSON jinja filters --- .../mistral-test-func-from-json-string.yaml | 16 +++++ .../mistral-test-func-from-yaml-string.yaml | 16 +++++ .../mistral-test-func-jmespath-query.yaml | 18 +++++ st2tests/integration/mistral/test_filters.py | 66 +++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 contrib/examples/actions/workflows/tests/mistral-test-func-from-json-string.yaml create mode 100644 contrib/examples/actions/workflows/tests/mistral-test-func-from-yaml-string.yaml create mode 100644 contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query.yaml diff --git a/contrib/examples/actions/workflows/tests/mistral-test-func-from-json-string.yaml b/contrib/examples/actions/workflows/tests/mistral-test-func-from-json-string.yaml new file mode 100644 index 0000000000..860b6fc077 --- /dev/null +++ b/contrib/examples/actions/workflows/tests/mistral-test-func-from-json-string.yaml @@ -0,0 +1,16 @@ +version: '2.0' + +examples.mistral-test-func-from-json-string: + description: A workflow for testing from_json_string custom filter in mistral + type: direct + input: + - input_str + output: + result_jinja: <% $.result_jinja %> + result_yaql: <% $.result_yaql %> + tasks: + task1: + action: std.noop + publish: + result_jinja: "{{ from_json_string(_.input_str) }}" + result_yaql: '<% from_json_string($.input_str) %>' diff --git a/contrib/examples/actions/workflows/tests/mistral-test-func-from-yaml-string.yaml b/contrib/examples/actions/workflows/tests/mistral-test-func-from-yaml-string.yaml new file mode 100644 index 0000000000..af45f2e233 --- /dev/null +++ b/contrib/examples/actions/workflows/tests/mistral-test-func-from-yaml-string.yaml @@ -0,0 +1,16 @@ +version: '2.0' + +examples.mistral-test-func-from-yaml-string: + description: A workflow for testing from_yaml_string custom filter in mistral + type: direct + input: + - input_str + output: + result_jinja: <% $.result_jinja %> + result_yaql: <% $.result_yaql %> + tasks: + task1: + action: std.noop + publish: + result_jinja: "{{ from_yaml_string(_.input_str) }}" + result_yaql: '<% from_yaml_string($.input_str) %>' diff --git a/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query.yaml b/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query.yaml new file mode 100644 index 0000000000..a578141168 --- /dev/null +++ b/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query.yaml @@ -0,0 +1,18 @@ +version: '2.0' + +examples.mistral-test-func-jmespath-query: + description: A workflow for testing jmespath_query custom filter in mistral + type: direct + input: + - input_obj + - input_query + output: + result_jinja: <% $.result_jinja %> + result_yaql: <% $.result_yaql %> + tasks: + + task2: + action: std.noop + publish: + result_jinja: '{{ jmespath_query(_.input_obj, _.input_query) }}' + result_yaql: '<% jmespath_query($.input_obj, $.input_query) %>' diff --git a/st2tests/integration/mistral/test_filters.py b/st2tests/integration/mistral/test_filters.py index 3d8694d90d..c0ea1a96a2 100644 --- a/st2tests/integration/mistral/test_filters.py +++ b/st2tests/integration/mistral/test_filters.py @@ -25,6 +25,72 @@ ] +class FromJsonStringFiltersTest(base.TestWorkflowExecution): + + def test_from_json_string(self): + + execution = self._execute_workflow( + 'examples.mistral-test-func-from-json-string', + parameters={ + "input_str": '{"a": "b"}' + } + ) + execution = self._wait_for_completion(execution) + self._assert_success(execution, num_tasks=1) + jinja_dict = execution.result['result_jinja'] + yaql_dict = execution.result['result_yaql'] + self.assertTrue(isinstance(jinja_dict, dict)) + self.assertEqual(jinja_dict["a"], "b") + self.assertTrue(isinstance(yaql_dict, dict)) + self.assertEqual(yaql_dict["a"], "b") + + +class FromYamlStringFiltersTest(base.TestWorkflowExecution): + + def test_to_yaml_string(self): + + execution = self._execute_workflow( + 'examples.mistral-test-func-from-yaml-string', + parameters={ + "input_str": 'a: b' + } + ) + execution = self._wait_for_completion(execution) + self._assert_success(execution, num_tasks=1) + jinja_dict = execution.result['result_jinja'] + yaql_dict = execution.result['result_yaql'] + self.assertTrue(isinstance(jinja_dict, dict)) + self.assertEqual(jinja_dict["a"], "b") + self.assertTrue(isinstance(yaql_dict, dict)) + self.assertEqual(yaql_dict["a"], "b") + + +class JmespathQueryFiltersTest(base.TestWorkflowExecution): + + def test_jmespath_query(self): + + execution = self._execute_workflow( + 'examples.mistral-test-func-jmespath-query', + parameters={ + "input_obj": {'people': [{'first': 'James', 'last': 'Smith'}, + {'first': 'Jacob', 'last': 'Alberts'}, + {'first': 'Jayden', 'last': 'Davis'}, + {'missing': 'different'}]} + "input_query": "people[*].last" + } + ) + expected_result = ['Smith', 'Alberts', 'Davis'] + + execution = self._wait_for_completion(execution) + self._assert_success(execution, num_tasks=1) + jinja_result = execution.result['result_jinja'] + yaql_result = execution.result['result_yaql'] + self.assertTrue(isinstance(jinja_result, list)) + self.assertEqual(jinja_result, expected_result) + self.assertTrue(isinstance(yaql_result, list)) + self.assertEqual(yaql_result, expected_result) + + class JsonEscapeFiltersTest(base.TestWorkflowExecution): def test_json_escape(self): From e095999a9480bf529ff49be5623d0c99f4331641 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 26 Sep 2017 22:46:55 -0400 Subject: [PATCH 04/12] Added integration tests for jmespath_query_str --- CHANGELOG.rst | 4 +++ .../mistral-test-func-jmespath-query-str.yaml | 18 +++++++++++++ st2tests/integration/mistral/test_filters.py | 25 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query-str.yaml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 18b7b2ead6..f311f7f0c5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,7 +35,11 @@ Added StackStorm role mappings. This means that the same role can now be granted via multiple RBAC mapping files. #3763 +* Add new Jinja filters ``from_json_string``, ``from_yaml_string``, ``jmespath_query``, + and ``jmespath_query_str``. + #3763 + Fixed ~~~~~ diff --git a/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query-str.yaml b/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query-str.yaml new file mode 100644 index 0000000000..4a6a483f68 --- /dev/null +++ b/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query-str.yaml @@ -0,0 +1,18 @@ +version: '2.0' + +examples.mistral-test-func-jmespath-query-str: + description: A workflow for testing jmespath_query_str custom filter in mistral + type: direct + input: + - input_str + - input_query + output: + result_jinja: <% $.result_jinja %> + result_yaql: <% $.result_yaql %> + tasks: + + task2: + action: std.noop + publish: + result_jinja: '{{ jmespath_query_str(_.input_str, _.input_query) }}' + result_yaql: '<% jmespath_query_str($.input_str, $.input_query) %>' diff --git a/st2tests/integration/mistral/test_filters.py b/st2tests/integration/mistral/test_filters.py index c0ea1a96a2..6dfa678da3 100644 --- a/st2tests/integration/mistral/test_filters.py +++ b/st2tests/integration/mistral/test_filters.py @@ -90,6 +90,31 @@ def test_jmespath_query(self): self.assertTrue(isinstance(yaql_result, list)) self.assertEqual(yaql_result, expected_result) + def test_jmespath_query_str(self): + + input_obj = {'people': [{'first': 'James', 'last': 'Smith'}, + {'first': 'Jacob', 'last': 'Alberts'}, + {'first': 'Jayden', 'last': 'Davis'}, + {'missing': 'different'}]} + input_str = json.dumps(input_obj) + execution = self._execute_workflow( + 'examples.mistral-test-func-jmespath-query-str', + parameters={ + "input_str": input_str, + "input_query": "people[*].last" + } + ) + expected_result = ['Smith', 'Alberts', 'Davis'] + + execution = self._wait_for_completion(execution) + self._assert_success(execution, num_tasks=1) + jinja_result = execution.result['result_jinja'] + yaql_result = execution.result['result_yaql'] + self.assertTrue(isinstance(jinja_result, list)) + self.assertEqual(jinja_result, expected_result) + self.assertTrue(isinstance(yaql_result, list)) + self.assertEqual(yaql_result, expected_result) + class JsonEscapeFiltersTest(base.TestWorkflowExecution): From b7490fa9bb0f3d324817d1293dce12b19b1b302a Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 26 Sep 2017 22:54:32 -0400 Subject: [PATCH 05/12] Removed jmespath_query_str since it was not necessary, instead we can simply chain filters together --- CHANGELOG.rst | 3 +-- .../mistral-test-func-jmespath-query-str.yaml | 18 ------------- .../st2common/jinja/filters/jmespath_query.py | 12 --------- ...est_jinja_render_jmespath_query_filters.py | 16 ------------ st2tests/integration/mistral/test_filters.py | 25 ------------------- 5 files changed, 1 insertion(+), 73 deletions(-) delete mode 100644 contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query-str.yaml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f311f7f0c5..1089ddc9e9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,8 +35,7 @@ Added StackStorm role mappings. This means that the same role can now be granted via multiple RBAC mapping files. #3763 -* Add new Jinja filters ``from_json_string``, ``from_yaml_string``, ``jmespath_query``, - and ``jmespath_query_str``. +* Add new Jinja filters ``from_json_string``, ``from_yaml_string``, and ``jmespath_query``. #3763 diff --git a/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query-str.yaml b/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query-str.yaml deleted file mode 100644 index 4a6a483f68..0000000000 --- a/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query-str.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: '2.0' - -examples.mistral-test-func-jmespath-query-str: - description: A workflow for testing jmespath_query_str custom filter in mistral - type: direct - input: - - input_str - - input_query - output: - result_jinja: <% $.result_jinja %> - result_yaql: <% $.result_yaql %> - tasks: - - task2: - action: std.noop - publish: - result_jinja: '{{ jmespath_query_str(_.input_str, _.input_query) }}' - result_yaql: '<% jmespath_query_str($.input_str, $.input_query) %>' diff --git a/st2common/st2common/jinja/filters/jmespath_query.py b/st2common/st2common/jinja/filters/jmespath_query.py index c8142bc9be..8c82525e4f 100644 --- a/st2common/st2common/jinja/filters/jmespath_query.py +++ b/st2common/st2common/jinja/filters/jmespath_query.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json import jmespath __all__ = [ @@ -31,14 +30,3 @@ def jmespath_query(value, query): :rtype: dict, array, int, string, bool """ return jmespath.search(query, value) - -def jmespath_query_str(value, query): - """Extracts data from a JSON string `value` using a jmespath `query`. - :link: http://jmespath.org - :param value: a json string to query - :param query: a jmsepath query expression (string) - :returns: the result of the query executed on the value - :rtype: dict, array, int, string, bool - """ - obj = json.loads(value) - return jmespath.search(query, obj) diff --git a/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py b/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py index 87aeff7328..316eb3b6ec 100644 --- a/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py +++ b/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py @@ -52,19 +52,3 @@ def test_jmespath_query_dynamic(self): actual = eval(actual_str) expected = ['d', 'e', 'f'] self.assertEqual(actual, expected) - - - def test_jmespath_query_str(self): - env = jinja_utils.get_jinja_environment() - obj = {'people': [{'first': 'James', 'last': 'd'}, - {'first': 'Jacob', 'last': 'e'}, - {'first': 'Jayden', 'last': 'f'}, - {'missing': 'different'}], - 'foo': {'bar': 'baz'}} - obj_json_str = json.dumps(obj) - - template = '{{ obj_str | jmespath_query_str("people[*].first") }}' - actual_str = env.from_string(template).render({'obj_str': obj_json_str}) - actual = eval(actual_str) - expected = ['James', 'Jacob', 'Jayden'] - self.assertEqual(actual, expected) diff --git a/st2tests/integration/mistral/test_filters.py b/st2tests/integration/mistral/test_filters.py index 6dfa678da3..c0ea1a96a2 100644 --- a/st2tests/integration/mistral/test_filters.py +++ b/st2tests/integration/mistral/test_filters.py @@ -90,31 +90,6 @@ def test_jmespath_query(self): self.assertTrue(isinstance(yaql_result, list)) self.assertEqual(yaql_result, expected_result) - def test_jmespath_query_str(self): - - input_obj = {'people': [{'first': 'James', 'last': 'Smith'}, - {'first': 'Jacob', 'last': 'Alberts'}, - {'first': 'Jayden', 'last': 'Davis'}, - {'missing': 'different'}]} - input_str = json.dumps(input_obj) - execution = self._execute_workflow( - 'examples.mistral-test-func-jmespath-query-str', - parameters={ - "input_str": input_str, - "input_query": "people[*].last" - } - ) - expected_result = ['Smith', 'Alberts', 'Davis'] - - execution = self._wait_for_completion(execution) - self._assert_success(execution, num_tasks=1) - jinja_result = execution.result['result_jinja'] - yaql_result = execution.result['result_yaql'] - self.assertTrue(isinstance(jinja_result, list)) - self.assertEqual(jinja_result, expected_result) - self.assertTrue(isinstance(yaql_result, list)) - self.assertEqual(yaql_result, expected_result) - class JsonEscapeFiltersTest(base.TestWorkflowExecution): From 8dc040fc75b11a649ddac2a031f97784dbc5e592 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 26 Sep 2017 23:02:06 -0400 Subject: [PATCH 06/12] Fix flake8 errors --- st2common/st2common/jinja/filters/jmespath_query.py | 1 - st2common/st2common/util/jinja.py | 3 +-- .../tests/unit/test_jinja_render_jmespath_query_filters.py | 2 -- st2tests/integration/mistral/test_filters.py | 2 +- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/st2common/st2common/jinja/filters/jmespath_query.py b/st2common/st2common/jinja/filters/jmespath_query.py index 8c82525e4f..22aba24e4a 100644 --- a/st2common/st2common/jinja/filters/jmespath_query.py +++ b/st2common/st2common/jinja/filters/jmespath_query.py @@ -17,7 +17,6 @@ __all__ = [ 'jmespath_query', - 'jmespath_query_str', ] diff --git a/st2common/st2common/util/jinja.py b/st2common/st2common/util/jinja.py index c88336f8c5..5ee46c033b 100644 --- a/st2common/st2common/util/jinja.py +++ b/st2common/st2common/util/jinja.py @@ -93,8 +93,7 @@ def get_filters(): 'use_none': use_none, 'json_escape': json_escape.json_escape, - 'jmespath_query': jmespath_query.jmespath_query, - 'jmespath_query_str': jmespath_query.jmespath_query_str + 'jmespath_query': jmespath_query.jmespath_query } diff --git a/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py b/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py index 316eb3b6ec..4bdb49e8c7 100644 --- a/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py +++ b/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py @@ -14,7 +14,6 @@ # limitations under the License. -import json import unittest2 from st2common.util import jinja as jinja_utils @@ -36,7 +35,6 @@ def test_jmespath_query_static(self): expected = ['James', 'Jacob', 'Jayden'] self.assertEqual(actual, expected) - def test_jmespath_query_dynamic(self): env = jinja_utils.get_jinja_environment() obj = {'people': [{'first': 'James', 'last': 'd'}, diff --git a/st2tests/integration/mistral/test_filters.py b/st2tests/integration/mistral/test_filters.py index c0ea1a96a2..6c5cd2c741 100644 --- a/st2tests/integration/mistral/test_filters.py +++ b/st2tests/integration/mistral/test_filters.py @@ -75,7 +75,7 @@ def test_jmespath_query(self): "input_obj": {'people': [{'first': 'James', 'last': 'Smith'}, {'first': 'Jacob', 'last': 'Alberts'}, {'first': 'Jayden', 'last': 'Davis'}, - {'missing': 'different'}]} + {'missing': 'different'}]}, "input_query": "people[*].last" } ) From f3abc88067d34bb0c945c0522b45877bab4781b1 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 26 Sep 2017 23:16:50 -0400 Subject: [PATCH 07/12] Fixed function name in from_yaml_string integration test --- st2tests/integration/mistral/test_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2tests/integration/mistral/test_filters.py b/st2tests/integration/mistral/test_filters.py index 6c5cd2c741..f5feb10f84 100644 --- a/st2tests/integration/mistral/test_filters.py +++ b/st2tests/integration/mistral/test_filters.py @@ -47,7 +47,7 @@ def test_from_json_string(self): class FromYamlStringFiltersTest(base.TestWorkflowExecution): - def test_to_yaml_string(self): + def test_from_yaml_string(self): execution = self._execute_workflow( 'examples.mistral-test-func-from-yaml-string', From 7cac87c100dbcc7957d4ec2dfe9307ecfc8328e8 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 26 Sep 2017 23:26:10 -0400 Subject: [PATCH 08/12] Add ci-mistral tests to travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fc2fede0c7..7777e29a33 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: matrix: - TASK=ci-unit NODE_INDEX=0 NODE_TOTAL=2 - TASK=ci-unit NODE_INDEX=1 NODE_TOTAL=2 - - TASK=ci-integration + - TASK="ci-integration ci-mistral" - TASK="ci-checks ci-packs-tests" addons: From 07794c1ea14eed95aa9180b1be9704177fca1ddc Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 26 Sep 2017 23:47:06 -0400 Subject: [PATCH 09/12] Removed ci-mistral tests from tavis (weren't enabled before) and extra newline in changelog --- .travis.yml | 2 +- CHANGELOG.rst | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7777e29a33..fc2fede0c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: matrix: - TASK=ci-unit NODE_INDEX=0 NODE_TOTAL=2 - TASK=ci-unit NODE_INDEX=1 NODE_TOTAL=2 - - TASK="ci-integration ci-mistral" + - TASK=ci-integration - TASK="ci-checks ci-packs-tests" addons: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1089ddc9e9..34cc8a2570 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,7 +37,6 @@ Added #3763 * Add new Jinja filters ``from_json_string``, ``from_yaml_string``, and ``jmespath_query``. #3763 - Fixed ~~~~~ From 568340ec5999e090dfe45602ec2144285767b1fe Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Thu, 28 Sep 2017 19:49:40 -0400 Subject: [PATCH 10/12] Migrated from JMESPath to JSONPath in the new jinja filters --- CHANGELOG.rst | 2 +- ... => mistral-test-func-jsonpath-query.yaml} | 8 +++--- fixed-requirements.txt | 1 - requirements.txt | 1 - st2common/in-requirements.txt | 1 - .../{jmespath_query.py => jsonpath_query.py} | 18 ++++++++----- st2common/st2common/util/jinja.py | 4 +-- ...st_jinja_render_jsonpath_query_filters.py} | 26 +++++++++++++++---- st2tests/integration/mistral/test_filters.py | 6 ++--- 9 files changed, 42 insertions(+), 25 deletions(-) rename contrib/examples/actions/workflows/tests/{mistral-test-func-jmespath-query.yaml => mistral-test-func-jsonpath-query.yaml} (56%) rename st2common/st2common/jinja/filters/{jmespath_query.py => jsonpath_query.py} (70%) rename st2common/tests/unit/{test_jinja_render_jmespath_query_filters.py => test_jinja_render_jsonpath_query_filters.py} (67%) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 34cc8a2570..10dab0586b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,7 +35,7 @@ Added StackStorm role mappings. This means that the same role can now be granted via multiple RBAC mapping files. #3763 -* Add new Jinja filters ``from_json_string``, ``from_yaml_string``, and ``jmespath_query``. +* Add new Jinja filters ``from_json_string``, ``from_yaml_string``, and ``jsonpath_query``. #3763 Fixed diff --git a/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query.yaml b/contrib/examples/actions/workflows/tests/mistral-test-func-jsonpath-query.yaml similarity index 56% rename from contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query.yaml rename to contrib/examples/actions/workflows/tests/mistral-test-func-jsonpath-query.yaml index a578141168..9d2002664d 100644 --- a/contrib/examples/actions/workflows/tests/mistral-test-func-jmespath-query.yaml +++ b/contrib/examples/actions/workflows/tests/mistral-test-func-jsonpath-query.yaml @@ -1,7 +1,7 @@ version: '2.0' -examples.mistral-test-func-jmespath-query: - description: A workflow for testing jmespath_query custom filter in mistral +examples.mistral-test-func-jsonpath-query: + description: A workflow for testing jsonpath_query custom filter in mistral type: direct input: - input_obj @@ -14,5 +14,5 @@ examples.mistral-test-func-jmespath-query: task2: action: std.noop publish: - result_jinja: '{{ jmespath_query(_.input_obj, _.input_query) }}' - result_yaql: '<% jmespath_query($.input_obj, $.input_query) %>' + result_jinja: '{{ jsonpath_query(_.input_obj, _.input_query) }}' + result_yaql: '<% jsonpath_query($.input_obj, $.input_query) %>' diff --git a/fixed-requirements.txt b/fixed-requirements.txt index 1ef7866a41..20c8cc5c02 100644 --- a/fixed-requirements.txt +++ b/fixed-requirements.txt @@ -15,7 +15,6 @@ pyyaml>=3.12,<4.0 requests[security]>=2.14.1,<2.15 apscheduler==3.3.1 gitpython==2.1.5 -jmespath<1.0.0,>=0.9.3 jsonschema==2.6.0 # Note: mongoengine v0.13.0 introduces memory usage regression so we can't # upgrade - https://github.com/StackStorm/st2/pull/3597 diff --git a/requirements.txt b/requirements.txt index aa7132fdb8..917a17908b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,6 @@ gitpython==2.1.5 gunicorn==19.7.1 ipaddr jinja2 -jmespath<1.0.0,>=0.9.3 jsonpath-rw==1.4.0 jsonschema==2.6.0 kombu==4.0.2 diff --git a/st2common/in-requirements.txt b/st2common/in-requirements.txt index 617cbef1ee..149062478a 100644 --- a/st2common/in-requirements.txt +++ b/st2common/in-requirements.txt @@ -3,7 +3,6 @@ apscheduler python-dateutil eventlet jinja2 -jmespath jsonschema kombu mongoengine diff --git a/st2common/st2common/jinja/filters/jmespath_query.py b/st2common/st2common/jinja/filters/jsonpath_query.py similarity index 70% rename from st2common/st2common/jinja/filters/jmespath_query.py rename to st2common/st2common/jinja/filters/jsonpath_query.py index 22aba24e4a..777a4cdeac 100644 --- a/st2common/st2common/jinja/filters/jmespath_query.py +++ b/st2common/st2common/jinja/filters/jsonpath_query.py @@ -13,19 +13,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -import jmespath +import jsonpath-rw __all__ = [ - 'jmespath_query', + 'jsonpath_query', ] -def jmespath_query(value, query): - """Extracts data from an object `value` using a jmespath `query`. - :link: http://jmespath.org +def jsonpath_query(value, query): + """Extracts data from an object `value` using a JSONPath `query`. + :link: https://github.com/kennknowles/python-jsonpath-rw :param value: a object (dict, array, etc) to query - :param query: a jmsepath query expression (string) + :param query: a JSONPath query expression (string) :returns: the result of the query executed on the value :rtype: dict, array, int, string, bool """ - return jmespath.search(query, value) + expr = jsonpath_rw.parse(query) + matches = [match.value for match in expr.find(obj)] + if not matches: + return None + return matches diff --git a/st2common/st2common/util/jinja.py b/st2common/st2common/util/jinja.py index 5ee46c033b..ffebbd319e 100644 --- a/st2common/st2common/util/jinja.py +++ b/st2common/st2common/util/jinja.py @@ -60,7 +60,7 @@ def get_filters(): from st2common.jinja.filters import time from st2common.jinja.filters import version from st2common.jinja.filters import json_escape - from st2common.jinja.filters import jmespath_query + from st2common.jinja.filters import jsonpath_query # IMPORTANT NOTE - these filters were recently duplicated in st2mistral so that # they are also available in Mistral workflows. Please ensure any additions you @@ -93,7 +93,7 @@ def get_filters(): 'use_none': use_none, 'json_escape': json_escape.json_escape, - 'jmespath_query': jmespath_query.jmespath_query + 'jsonpath_query': jsonpath_query.jsonpath_query } diff --git a/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py b/st2common/tests/unit/test_jinja_render_jsonpath_query_filters.py similarity index 67% rename from st2common/tests/unit/test_jinja_render_jmespath_query_filters.py rename to st2common/tests/unit/test_jinja_render_jsonpath_query_filters.py index 4bdb49e8c7..d75ed5e84f 100644 --- a/st2common/tests/unit/test_jinja_render_jmespath_query_filters.py +++ b/st2common/tests/unit/test_jinja_render_jsonpath_query_filters.py @@ -19,9 +19,9 @@ from st2common.util import jinja as jinja_utils -class JinjaUtilsJmespathQueryTestCase(unittest2.TestCase): +class JinjaUtilsJsonpathQueryTestCase(unittest2.TestCase): - def test_jmespath_query_static(self): + def test_jsonpath_query_static(self): env = jinja_utils.get_jinja_environment() obj = {'people': [{'first': 'James', 'last': 'd'}, {'first': 'Jacob', 'last': 'e'}, @@ -29,13 +29,13 @@ def test_jmespath_query_static(self): {'missing': 'different'}], 'foo': {'bar': 'baz'}} - template = '{{ obj | jmespath_query("people[*].first") }}' + template = '{{ obj | jsonpath_query("people[*].first") }}' actual_str = env.from_string(template).render({'obj': obj}) actual = eval(actual_str) expected = ['James', 'Jacob', 'Jayden'] self.assertEqual(actual, expected) - def test_jmespath_query_dynamic(self): + def test_jsonpath_query_dynamic(self): env = jinja_utils.get_jinja_environment() obj = {'people': [{'first': 'James', 'last': 'd'}, {'first': 'Jacob', 'last': 'e'}, @@ -44,9 +44,25 @@ def test_jmespath_query_dynamic(self): 'foo': {'bar': 'baz'}} query = "people[*].last" - template = '{{ obj | jmespath_query(query) }}' + template = '{{ obj | jsonpath_query(query) }}' actual_str = env.from_string(template).render({'obj': obj, 'query': query}) actual = eval(actual_str) expected = ['d', 'e', 'f'] self.assertEqual(actual, expected) + + def test_jsonpath_query_no_results(self): + env = jinja_utils.get_jinja_environment() + obj = {'people': [{'first': 'James', 'last': 'd'}, + {'first': 'Jacob', 'last': 'e'}, + {'first': 'Jayden', 'last': 'f'}, + {'missing': 'different'}], + 'foo': {'bar': 'baz'}} + query = "query_returns_no_results" + + template = '{{ obj | jsonpath_query(query) }}' + actual_str = env.from_string(template).render({'obj': obj, + 'query': query}) + actual = eval(actual_str) + expected = None + self.assertEqual(actual, expected) diff --git a/st2tests/integration/mistral/test_filters.py b/st2tests/integration/mistral/test_filters.py index f5feb10f84..28c26b506f 100644 --- a/st2tests/integration/mistral/test_filters.py +++ b/st2tests/integration/mistral/test_filters.py @@ -65,12 +65,12 @@ def test_from_yaml_string(self): self.assertEqual(yaql_dict["a"], "b") -class JmespathQueryFiltersTest(base.TestWorkflowExecution): +class JsonpathQueryFiltersTest(base.TestWorkflowExecution): - def test_jmespath_query(self): + def test_jsonpath_query(self): execution = self._execute_workflow( - 'examples.mistral-test-func-jmespath-query', + 'examples.mistral-test-func-jsonpath-query', parameters={ "input_obj": {'people': [{'first': 'James', 'last': 'Smith'}, {'first': 'Jacob', 'last': 'Alberts'}, From a52c07b3f237d1d8859d69697f42a76aa1af112e Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Thu, 28 Sep 2017 19:55:34 -0400 Subject: [PATCH 11/12] Fixed a bugs in jsonpath query found during unit testing. --- .../st2common/jinja/filters/jsonpath_query.py | 2 +- st2tests/integration/mistral/test_filters.py | 38 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/st2common/st2common/jinja/filters/jsonpath_query.py b/st2common/st2common/jinja/filters/jsonpath_query.py index 777a4cdeac..878f9631d1 100644 --- a/st2common/st2common/jinja/filters/jsonpath_query.py +++ b/st2common/st2common/jinja/filters/jsonpath_query.py @@ -29,7 +29,7 @@ def jsonpath_query(value, query): :rtype: dict, array, int, string, bool """ expr = jsonpath_rw.parse(query) - matches = [match.value for match in expr.find(obj)] + matches = [match.value for match in expr.find(value)] if not matches: return None return matches diff --git a/st2tests/integration/mistral/test_filters.py b/st2tests/integration/mistral/test_filters.py index 28c26b506f..4c06258c20 100644 --- a/st2tests/integration/mistral/test_filters.py +++ b/st2tests/integration/mistral/test_filters.py @@ -65,6 +65,25 @@ def test_from_yaml_string(self): self.assertEqual(yaql_dict["a"], "b") +class JsonEscapeFiltersTest(base.TestWorkflowExecution): + + def test_json_escape(self): + + breaking_str = 'This text """ breaks JSON' + inputs = {'input_str': breaking_str} + execution = self._execute_workflow( + 'examples.mistral-test-func-json-escape', parameters=inputs + ) + execution = self._wait_for_completion(execution) + self._assert_success(execution, num_tasks=1) + jinja_dict = json.loads(execution.result['result_jinja'])[0] + yaql_dict = json.loads(execution.result['result_yaql'])[0] + self.assertTrue(isinstance(jinja_dict, dict)) + self.assertEqual(jinja_dict["title"], breaking_str) + self.assertTrue(isinstance(yaql_dict, dict)) + self.assertEqual(yaql_dict["title"], breaking_str) + + class JsonpathQueryFiltersTest(base.TestWorkflowExecution): def test_jsonpath_query(self): @@ -91,25 +110,6 @@ def test_jsonpath_query(self): self.assertEqual(yaql_result, expected_result) -class JsonEscapeFiltersTest(base.TestWorkflowExecution): - - def test_json_escape(self): - - breaking_str = 'This text """ breaks JSON' - inputs = {'input_str': breaking_str} - execution = self._execute_workflow( - 'examples.mistral-test-func-json-escape', parameters=inputs - ) - execution = self._wait_for_completion(execution) - self._assert_success(execution, num_tasks=1) - jinja_dict = json.loads(execution.result['result_jinja'])[0] - yaql_dict = json.loads(execution.result['result_yaql'])[0] - self.assertTrue(isinstance(jinja_dict, dict)) - self.assertEqual(jinja_dict["title"], breaking_str) - self.assertTrue(isinstance(yaql_dict, dict)) - self.assertEqual(yaql_dict["title"], breaking_str) - - class RegexMatchFiltersTest(base.TestWorkflowExecution): def test_regex_match(self): From 8c8018f592e9b5c5854eb1fec58e7db6a8b38733 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Thu, 28 Sep 2017 20:02:39 -0400 Subject: [PATCH 12/12] Fixed jsonpath import --- st2common/st2common/jinja/filters/jsonpath_query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2common/st2common/jinja/filters/jsonpath_query.py b/st2common/st2common/jinja/filters/jsonpath_query.py index 878f9631d1..61b9345c17 100644 --- a/st2common/st2common/jinja/filters/jsonpath_query.py +++ b/st2common/st2common/jinja/filters/jsonpath_query.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import jsonpath-rw +import jsonpath_rw __all__ = [ 'jsonpath_query',