From f25a724ade95bc5b331f083bab94dc1279aeabc2 Mon Sep 17 00:00:00 2001 From: Alexander Kislitsky Date: Thu, 18 Aug 2016 18:57:13 +0300 Subject: [PATCH] Crud operations for hierarchy levels implemented Only name changing implemented under hierarchy_levels url. Whole hierarchy change should be performed throught environment update. Cascade deletion added to hierarchy levels on environment deletion. Order of hierarchy levels fixed for environments GET requests. Module levels_hierarchy was renamed to hierarchy_levels. Change-Id: I0642892b517357ebc95427617413048f4db6fba3 --- tuning_box/app.py | 12 ++ tuning_box/db.py | 29 ++++- tuning_box/library/__init__.py | 4 +- tuning_box/library/environments.py | 46 +++++-- tuning_box/library/hierarchy_levels.py | 85 +++++++++++++ tuning_box/library/levels_hierarchy.py | 38 ------ tuning_box/library/resource_overrides.py | 6 +- tuning_box/library/resource_values.py | 6 +- .../adf671eddeb4_level_cascade_deletion.py | 59 +++++++++ tuning_box/tests/library/test_environments.py | 56 ++++++++- .../tests/library/test_hierarchy_levels.py | 118 ++++++++++++++++++ .../tests/library/test_levels_hierarchy.py | 58 --------- 12 files changed, 396 insertions(+), 121 deletions(-) create mode 100644 tuning_box/library/hierarchy_levels.py delete mode 100644 tuning_box/library/levels_hierarchy.py create mode 100644 tuning_box/migrations/versions/adf671eddeb4_level_cascade_deletion.py create mode 100644 tuning_box/tests/library/test_hierarchy_levels.py delete mode 100644 tuning_box/tests/library/test_levels_hierarchy.py diff --git a/tuning_box/app.py b/tuning_box/app.py index 8debe77..61dac38 100644 --- a/tuning_box/app.py +++ b/tuning_box/app.py @@ -19,6 +19,7 @@ from tuning_box import db from tuning_box import errors from tuning_box.library import components from tuning_box.library import environments +from tuning_box.library import hierarchy_levels from tuning_box.library import resource_definitions from tuning_box.library import resource_overrides from tuning_box.library import resource_values @@ -86,6 +87,17 @@ api.add_resource( '/environments/' ) +# Hierarchy levels +api.add_resource( + hierarchy_levels.EnvironmentHierarchyLevelsCollection, + '/environments//hierarchy_levels' +) +api.add_resource( + hierarchy_levels.EnvironmentHierarchyLevels, + '/environments//hierarchy_levels/' + '' +) + def handle_integrity_error(exc): response = flask.jsonify(msg=exc.args[0]) diff --git a/tuning_box/db.py b/tuning_box/db.py index 8d59e16..7d15199 100644 --- a/tuning_box/db.py +++ b/tuning_box/db.py @@ -16,11 +16,14 @@ import re import flask import flask_sqlalchemy +import sqlalchemy import sqlalchemy.event import sqlalchemy.ext.declarative as sa_decl from sqlalchemy.orm import exc as orm_exc from sqlalchemy import types +from tuning_box import errors + try: from importlib import reload except ImportError: @@ -152,8 +155,12 @@ class Environment(ModelMixin, db.Model): class EnvironmentHierarchyLevel(ModelMixin, db.Model): - environment_id = fk(Environment) - environment = db.relationship(Environment, backref='hierarchy_levels') + environment_id = fk(Environment, ondelete='CASCADE') + environment = db.relationship( + Environment, + backref=sqlalchemy.orm.backref('hierarchy_levels', + cascade="all, delete-orphan") + ) name = db.Column(db.String(128)) @sa_decl.declared_attr @@ -245,3 +252,21 @@ def prefix_tables(module, prefix): def unprefix_tables(module): ModelMixin.table_prefix = "" reload(module) + + +def get_or_404(cls, ident): + result = cls.query.get(ident) + if result is None: + raise errors.TuningboxNotFound( + "{0} not found by {1}".format(cls.__name__, ident) + ) + return result + + +def find_or_404(cls, **attrs): + item = cls.query.filter_by(**attrs).one_or_none() + if not item: + raise errors.TuningboxNotFound( + "{0} not found by {1}".format(cls.__name__, attrs) + ) + return item diff --git a/tuning_box/library/__init__.py b/tuning_box/library/__init__.py index 3b2f474..ba2c7b9 100644 --- a/tuning_box/library/__init__.py +++ b/tuning_box/library/__init__.py @@ -14,7 +14,7 @@ from sqlalchemy.orm import exc as sa_exc from tuning_box import db from tuning_box import errors -from tuning_box.library import levels_hierarchy +from tuning_box.library import hierarchy_levels def load_objects(model, ids): @@ -75,7 +75,7 @@ def get_resource_definition(id_or_name, environment_id): def get_resource_values(environment, levels, res_def): - level_value = levels_hierarchy.get_environment_level_value( + level_value = hierarchy_levels.get_environment_level_value( environment, levels) res_values = db.ResourceValues.query.filter_by( environment_id=environment.id, diff --git a/tuning_box/library/environments.py b/tuning_box/library/environments.py index 0934ae1..5d8de96 100644 --- a/tuning_box/library/environments.py +++ b/tuning_box/library/environments.py @@ -29,7 +29,15 @@ class EnvironmentsCollection(flask_restful.Resource): method_decorators = [flask_restful.marshal_with(environment_fields)] def get(self): - return db.Environment.query.all() + envs = db.Environment.query.all() + result = [] + for env in envs: + hierarchy_levels = db.EnvironmentHierarchyLevel.\ + get_for_environment(env) + # Proper order of levels can't be provided by ORM backref + result.append({'id': env.id, 'components': env.components, + 'hierarchy_levels': hierarchy_levels}) + return result, 200 def _check_components(self, components): identities = set() @@ -72,7 +80,12 @@ class Environment(flask_restful.Resource): method_decorators = [flask_restful.marshal_with(environment_fields)] def get(self, environment_id): - return db.Environment.query.get_or_404(environment_id) + env = db.Environment.query.get_or_404(environment_id) + hierarchy_levels = db.EnvironmentHierarchyLevel.\ + get_for_environment(env) + # Proper order of levels can't be provided by ORM backref + return {'id': env.id, 'components': env.components, + 'hierarchy_levels': hierarchy_levels}, 200 def _update_components(self, environment, components): if components is not None: @@ -80,14 +93,29 @@ class Environment(flask_restful.Resource): db.Component, components) environment.components = new_components - def _update_hierarchy_levels(self, environment, hierarchy_levels): - if hierarchy_levels is not None: - new_hierarchy_levels = library.load_objects_by_id_or_name( - db.EnvironmentHierarchyLevel, hierarchy_levels) - parent = None + def _update_hierarchy_levels(self, environment, hierarchy_levels_names): + if hierarchy_levels_names is not None: + old_hierarchy_levels = db.EnvironmentHierarchyLevel.query.filter( + db.EnvironmentHierarchyLevel.environment_id == environment.id + ).all() + + new_hierarchy_levels = [] + + for level_name in hierarchy_levels_names: + level = db.get_or_create( + db.EnvironmentHierarchyLevel, + name=level_name, + environment=environment + ) + new_hierarchy_levels.append(level) + + parent_id = None for level in new_hierarchy_levels: - level.parent = parent - parent = level + level.parent_id = parent_id + parent_id = level.id + for old_level in old_hierarchy_levels: + if old_level not in new_hierarchy_levels: + db.db.session.delete(old_level) environment.hierarchy_levels = new_hierarchy_levels @db.with_transaction diff --git a/tuning_box/library/hierarchy_levels.py b/tuning_box/library/hierarchy_levels.py new file mode 100644 index 0000000..18841c6 --- /dev/null +++ b/tuning_box/library/hierarchy_levels.py @@ -0,0 +1,85 @@ +# 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 flask +import werkzeug + +import flask_restful +from flask_restful import fields + +from tuning_box import db + + +def iter_environment_level_values(environment, levels): + env_levels = db.EnvironmentHierarchyLevel.get_for_environment(environment) + level_pairs = zip(env_levels, levels) + for env_level, (level_name, level_value) in level_pairs: + if env_level.name != level_name: + raise werkzeug.exceptions.BadRequest( + "Unexpected level name '{0}'. Expected '{1}'.".format( + level_name, env_level.name)) + level_value_db = db.get_or_create( + db.EnvironmentHierarchyLevelValue, + level=env_level, + value=level_value, + ) + yield level_value_db + + +def get_environment_level_value(environment, levels): + level_value = None + for level_value in iter_environment_level_values(environment, levels): + pass + return level_value + + +environment_hierarchy_level_fields = { + 'name': fields.String, + 'environment_id': fields.Integer, + 'parent': fields.String(attribute='parent.name') +} + + +class EnvironmentHierarchyLevelsCollection(flask_restful.Resource): + method_decorators = [ + flask_restful.marshal_with(environment_hierarchy_level_fields) + ] + + def get(self, environment_id): + env = db.get_or_404(db.Environment, environment_id) + return db.EnvironmentHierarchyLevel.get_for_environment(env) + + +class EnvironmentHierarchyLevels(flask_restful.Resource): + method_decorators = [ + flask_restful.marshal_with(environment_hierarchy_level_fields) + ] + + def get(self, environment_id, level): + level = db.find_or_404(db.EnvironmentHierarchyLevel, + environment_id=environment_id, + name=level) + return level + + @db.with_transaction + def _do_update(self, environment_id, level): + level = db.find_or_404(db.EnvironmentHierarchyLevel, + environment_id=environment_id, + name=level) + level.name = flask.request.json.get('name', level.name) + + def put(self, environment_id, level): + return self.patch(environment_id, level) + + def patch(self, environment_id, level): + self._do_update(environment_id, level) + return None, 204 diff --git a/tuning_box/library/levels_hierarchy.py b/tuning_box/library/levels_hierarchy.py deleted file mode 100644 index d83bea7..0000000 --- a/tuning_box/library/levels_hierarchy.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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 werkzeug - -from tuning_box import db - - -def iter_environment_level_values(environment, levels): - env_levels = db.EnvironmentHierarchyLevel.get_for_environment(environment) - level_pairs = zip(env_levels, levels) - for env_level, (level_name, level_value) in level_pairs: - if env_level.name != level_name: - raise werkzeug.exceptions.BadRequest( - "Unexpected level name '{0}'. Expected '{1}'.".format( - level_name, env_level.name)) - level_value_db = db.get_or_create( - db.EnvironmentHierarchyLevelValue, - level=env_level, - value=level_value, - ) - yield level_value_db - - -def get_environment_level_value(environment, levels): - level_value = None - for level_value in iter_environment_level_values(environment, levels): - pass - return level_value diff --git a/tuning_box/library/resource_overrides.py b/tuning_box/library/resource_overrides.py index 39d25bf..8889441 100644 --- a/tuning_box/library/resource_overrides.py +++ b/tuning_box/library/resource_overrides.py @@ -15,7 +15,7 @@ import flask_restful from tuning_box import db from tuning_box import library -from tuning_box.library import levels_hierarchy +from tuning_box.library import hierarchy_levels from tuning_box.library import resource_keys_operation @@ -35,7 +35,7 @@ class ResourceOverrides(flask_restful.Resource): resource_id_or_name=res_def.id, ), code=308) - level_value = levels_hierarchy.get_environment_level_value( + level_value = hierarchy_levels.get_environment_level_value( environment, levels) esv = db.get_or_create( db.ResourceValues, @@ -61,7 +61,7 @@ class ResourceOverrides(flask_restful.Resource): ) return flask.redirect(url, code=308) - level_value = levels_hierarchy.get_environment_level_value( + level_value = hierarchy_levels.get_environment_level_value( environment, levels) res_values = db.ResourceValues.query.filter_by( resource_definition=res_def, diff --git a/tuning_box/library/resource_values.py b/tuning_box/library/resource_values.py index 86f4dd1..687fb9e 100644 --- a/tuning_box/library/resource_values.py +++ b/tuning_box/library/resource_values.py @@ -16,7 +16,7 @@ import itertools from tuning_box import db from tuning_box import library -from tuning_box.library import levels_hierarchy +from tuning_box.library import hierarchy_levels from tuning_box.library import resource_keys_operation @@ -37,7 +37,7 @@ class ResourceValues(flask_restful.Resource): resource_id_or_name=res_def.id, ), code=308) - level_value = levels_hierarchy.get_environment_level_value( + level_value = hierarchy_levels.get_environment_level_value( environment, levels) esv = db.get_or_create( db.ResourceValues, @@ -66,7 +66,7 @@ class ResourceValues(flask_restful.Resource): url += '?' + qs return flask.redirect(url, code=308) - level_values = list(levels_hierarchy.iter_environment_level_values( + level_values = list(hierarchy_levels.iter_environment_level_values( environment, levels)) if 'effective' in flask.request.args: diff --git a/tuning_box/migrations/versions/adf671eddeb4_level_cascade_deletion.py b/tuning_box/migrations/versions/adf671eddeb4_level_cascade_deletion.py new file mode 100644 index 0000000..0007f7f --- /dev/null +++ b/tuning_box/migrations/versions/adf671eddeb4_level_cascade_deletion.py @@ -0,0 +1,59 @@ +# 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. + +"""Level cascade deletion on environment removal + +Revision ID: adf671eddeb4 +Revises: a86472389a70 +Create Date: 2016-08-19 16:39:46.745113 + +""" + +# revision identifiers, used by Alembic. +revision = 'adf671eddeb4' +down_revision = 'a86472389a70' +branch_labels = None +depends_on = None + +from alembic import context +from alembic import op + + +def upgrade(): + table_prefix = context.config.get_main_option('table_prefix') + table_name = table_prefix + 'environment_hierarchy_level' + + with op.batch_alter_table(table_name) as batch: + batch.drop_constraint( + table_prefix + 'environment_hierarchy_level_environment_id_fkey', + type_='foreignkey' + ) + batch.create_foreign_key( + table_prefix + 'environment_hierarchy_level_environment_id_fkey', + table_prefix + 'environment', + ['environment_id'], ['id'], ondelete='CASCADE' + ) + + +def downgrade(): + table_prefix = context.config.get_main_option('table_prefix') + table_name = table_prefix + 'environment_hierarchy_level' + with op.batch_alter_table(table_name) as batch: + batch.drop_constraint( + table_prefix + 'environment_hierarchy_level_environment_id_fkey', + type_='foreignkey' + ) + batch.create_foreign_key( + table_prefix + 'environment_hierarchy_level_environment_id_fkey', + table_prefix + 'environment', + ['environment_id'], ['id'] + ) diff --git a/tuning_box/tests/library/test_environments.py b/tuning_box/tests/library/test_environments.py index 5ebb08f..06a720e 100644 --- a/tuning_box/tests/library/test_environments.py +++ b/tuning_box/tests/library/test_environments.py @@ -166,11 +166,24 @@ class TestEnvironments(BaseTest): def test_delete_environment(self): self._fixture() env_id = 9 - res = self.client.delete(self.object_url.format(env_id)) - self.assertEqual(res.status_code, 204) + env_url = self.object_url.format(env_id) + res = self.client.get(env_url) + self.assertEqual(200, res.status_code) + levels = ['lvl1', 'lvl2'] + self.assertEqual(levels, res.json['hierarchy_levels']) + + res = self.client.delete(env_url) + self.assertEqual(204, res.status_code) self.assertEqual(res.data, b'') self._assert_not_in_db(db.Environment, 9) + with self.app.app_context(): + for name in levels: + obj = db.EnvironmentHierarchyLevel.query.filter( + db.EnvironmentHierarchyLevel.name == name + ).first() + self.assertIsNone(obj) + def test_delete_environment_404(self): env_id = 9 res = self.client.delete(self.object_url.format(env_id)) @@ -260,11 +273,42 @@ class TestEnvironments(BaseTest): self.assertEqual(expected_levels, actual['hierarchy_levels']) self.check_hierarchy_levels(actual['hierarchy_levels']) - def test_put_environment_level_not_found(self): + def test_put_environment_hierarchy_levels_reverse(self): self._fixture() env_id = 9 + env_url = self.object_url.format(env_id) + initial = self.client.get(env_url).json + expected_levels = initial['hierarchy_levels'] + expected_levels.reverse() + + # Updating hierarchy levels res = self.client.put( - self.object_url.format(env_id), - data={'hierarchy_levels': [None]} + env_url, + data={'hierarchy_levels': expected_levels} ) - self.assertEqual(404, res.status_code) + self.assertEqual(204, res.status_code) + actual = self.client.get(env_url).json + self.assertEqual(expected_levels, actual['hierarchy_levels']) + self.check_hierarchy_levels(actual['hierarchy_levels']) + + def test_put_environment_hierarchy_levels_with_new_level(self): + self._fixture() + env_id = 9 + env_url = self.object_url.format(env_id) + initial = self.client.get(env_url).json + expected_levels = ['root'] + initial['hierarchy_levels'] + + res = self.client.put( + env_url, + data={'hierarchy_levels': expected_levels} + ) + self.assertEqual(204, res.status_code) + + res = self.client.get('/environments/9/hierarchy_levels') + self.assertEqual(200, res.status_code) + + res = self.client.get(env_url) + self.assertEqual(200, res.status_code) + actual = res.json + self.assertEqual(expected_levels, actual['hierarchy_levels']) + self.check_hierarchy_levels(actual['hierarchy_levels']) diff --git a/tuning_box/tests/library/test_hierarchy_levels.py b/tuning_box/tests/library/test_hierarchy_levels.py new file mode 100644 index 0000000..3fcb68e --- /dev/null +++ b/tuning_box/tests/library/test_hierarchy_levels.py @@ -0,0 +1,118 @@ +# 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 werkzeug + +from tuning_box import db +from tuning_box.library import hierarchy_levels +from tuning_box.tests.test_app import BaseTest + + +class TestLevelsHierarchy(BaseTest): + + collection_url = '/environments/{0}/hierarchy_levels' + object_url = collection_url + '/{1}' + + def test_get_environment_level_value_root(self): + self._fixture() + with self.app.app_context(), db.db.session.begin(): + level_value = hierarchy_levels.get_environment_level_value( + db.Environment(id=9), + [], + ) + self.assertIsNone(level_value) + + def test_get_environment_level_value_deep(self): + self._fixture() + with self.app.app_context(), db.db.session.begin(): + level_value = hierarchy_levels.get_environment_level_value( + db.Environment(id=9), + [('lvl1', 'val1'), ('lvl2', 'val2')], + ) + self.assertIsNotNone(level_value) + self.assertEqual(level_value.level.name, 'lvl2') + self.assertEqual(level_value.value, 'val2') + level = level_value.level.parent + self.assertIsNotNone(level) + self.assertEqual(level.name, 'lvl1') + self.assertIsNone(level.parent) + + def test_get_environment_level_value_bad_level(self): + self._fixture() + with self.app.app_context(), db.db.session.begin(): + exc = self.assertRaises( + werkzeug.exceptions.BadRequest, + hierarchy_levels.get_environment_level_value, + db.Environment(id=9), + [('lvlx', 'val1')], + ) + self.assertEqual( + exc.description, + "Unexpected level name 'lvlx'. Expected 'lvl1'.", + ) + + def test_get_hierarchy_levels(self): + self._fixture() + environment_id = 9 + expected_levels = ['lvl1', 'lvl2'] + res = self.client.get(self.collection_url.format(environment_id)) + self.assertEqual(200, res.status_code) + self.assertEqual(expected_levels, [d['name'] for d in res.json]) + + def test_get_hierarchy_levels_not_found(self): + environment_id = 9 + res = self.client.get(self.collection_url.format(environment_id)) + self.assertEqual(404, res.status_code) + + def test_get_hierarchy_level(self): + self._fixture() + environment_id = 9 + levels = ['lvl1', 'lvl2'] + for level in levels: + res = self.client.get(self.object_url.format(environment_id, + level)) + self.assertEqual(200, res.status_code) + self.assertEqual(level, res.json['name']) + + def test_get_hierarchy_level_not_found(self): + levels = ['lvl1', 'lvl2'] + for level in levels: + res = self.client.get(self.object_url.format(9, level)) + self.assertEqual(404, res.status_code) + + def test_put_hierarchy_level(self): + self._fixture() + environment_id = 9 + level = 'lvl1' + new_name = 'new_{0}'.format(level) + res = self.client.put(self.object_url.format(environment_id, level), + data={'name': new_name}) + self.assertEqual(204, res.status_code) + + res = self.client.get(self.object_url.format(environment_id, new_name)) + self.assertEqual(200, res.status_code) + self.assertEqual(new_name, res.json['name']) + + def test_put_hierarchy_level_not_found(self): + self._fixture() + environment_id = 9 + res = self.client.put(self.object_url.format(environment_id, 'xx'), + data={'name': 'new_name'}) + self.assertEqual(404, res.status_code) + + res = self.client.put(self.object_url.format(1, 'lvl1'), + data={'name': 'new_name'}) + self.assertEqual(404, res.status_code) + + res = self.client.put(self.object_url.format(1, 'xx'), + data={'name': 'new_name'}) + self.assertEqual(404, res.status_code) diff --git a/tuning_box/tests/library/test_levels_hierarchy.py b/tuning_box/tests/library/test_levels_hierarchy.py deleted file mode 100644 index a160015..0000000 --- a/tuning_box/tests/library/test_levels_hierarchy.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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 werkzeug - -from tuning_box import db -from tuning_box.library import levels_hierarchy -from tuning_box.tests.test_app import BaseTest - - -class TestLevelsHierarchy(BaseTest): - - def test_get_environment_level_value_root(self): - self._fixture() - with self.app.app_context(), db.db.session.begin(): - level_value = levels_hierarchy.get_environment_level_value( - db.Environment(id=9), - [], - ) - self.assertIsNone(level_value) - - def test_get_environment_level_value_deep(self): - self._fixture() - with self.app.app_context(), db.db.session.begin(): - level_value = levels_hierarchy.get_environment_level_value( - db.Environment(id=9), - [('lvl1', 'val1'), ('lvl2', 'val2')], - ) - self.assertIsNotNone(level_value) - self.assertEqual(level_value.level.name, 'lvl2') - self.assertEqual(level_value.value, 'val2') - level = level_value.level.parent - self.assertIsNotNone(level) - self.assertEqual(level.name, 'lvl1') - self.assertIsNone(level.parent) - - def test_get_environment_level_value_bad_level(self): - self._fixture() - with self.app.app_context(), db.db.session.begin(): - exc = self.assertRaises( - werkzeug.exceptions.BadRequest, - levels_hierarchy.get_environment_level_value, - db.Environment(id=9), - [('lvlx', 'val1')], - ) - self.assertEqual( - exc.description, - "Unexpected level name 'lvlx'. Expected 'lvl1'.", - )