Flake requirements

* Added Apache Licence for each file.
* Pep8 fixes

Change-Id: Iba54681c048c612e4b7e5859edf4f3713934f290
This commit is contained in:
Ukov Dmitry 2016-09-08 12:39:11 +03:00
parent bad515ff64
commit e63ae730a3
17 changed files with 262 additions and 62 deletions

View File

@ -1 +1,13 @@
# 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.
REPOS_DIR = '/var/lib/fuel_repos' REPOS_DIR = '/var/lib/fuel_repos'

View File

@ -1,24 +1,37 @@
# 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 copy
import os import os
import yaml import yaml
import copy
from nailgun.logger import logger
from nailgun.extensions import BaseExtension
from nailgun.extensions import BasePipeline
from fuel_external_git import const
from fuel_external_git import handlers from fuel_external_git import handlers
from fuel_external_git.objects import GitRepo from fuel_external_git.objects import GitRepo
from fuel_external_git.settings import GitExtensionSettings from fuel_external_git.settings import GitExtensionSettings
from fuel_external_git import const
from fuel_external_git import utils from fuel_external_git import utils
from nailgun.extensions import BaseExtension
from nailgun.extensions import BasePipeline
from nailgun.logger import logger
class OpenStackConfigPipeline(BasePipeline): class OpenStackConfigPipeline(BasePipeline):
# TODO (dukov) add cluster remove callback # TODO(dukov) add cluster remove callback
@classmethod @classmethod
def process_deployment(cls, data, cluster, nodes, **kwargs): def process_deployment(cls, data, cluster, nodes, **kwargs):
"""Genereate OpenStack configuration hash based on configuration files """Updating deployment info
Genereate OpenStack configuration hash based on configuration files
stored in git repository associated with a particular environment stored in git repository associated with a particular environment
Example of configuration extension: Example of configuration extension:
configuration: configuration:
@ -91,7 +104,8 @@ class OpenStackConfigPipeline(BasePipeline):
logger.info("Node {0} config from git {1}".format(uid, common)) logger.info("Node {0} config from git {1}".format(uid, common))
return data return data
# TODO (dukov) Remove decorator extension management is available
# TODO(dukov) Remove decorator extension management is available
@utils.register_extension(u'fuel_external_git') @utils.register_extension(u'fuel_external_git')
class ExternalGit(BaseExtension): class ExternalGit(BaseExtension):
name = 'fuel_external_git' name = 'fuel_external_git'

View File

@ -1,10 +1,23 @@
# 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.
from __future__ import absolute_import from __future__ import absolute_import
import os
import fabric.api import fabric.api
from git import Repo import os
from cliff import command from cliff import command
from cliff import lister from cliff import lister
from git import Repo
from fuelclient.client import APIClient from fuelclient.client import APIClient
from fuelclient.common import data_utils from fuelclient.common import data_utils
@ -88,7 +101,7 @@ class AddRepo(command.Command):
with open(parsed_args.key) as key_file: with open(parsed_args.key) as key_file:
data['user_key'] = key_file.read() data['user_key'] = key_file.read()
result = APIClient.post_request('/clusters/git-repos/', data) APIClient.post_request('/clusters/git-repos/', data)
return (self.columns, data) return (self.columns, data)
@ -120,7 +133,7 @@ class DeleteRepo(command.Command):
if repo['id'] == parsed_args.repo][0] if repo['id'] == parsed_args.repo][0]
del_path = "/clusters/{0}/git-repos/{1}" del_path = "/clusters/{0}/git-repos/{1}"
result = APIClient.delete_request(del_path.format(env, repo_id)) APIClient.delete_request(del_path.format(env, repo_id))
return (self.columns, {}) return (self.columns, {})
@ -208,7 +221,7 @@ class InitRepo(command.Command):
if repo['id'] == parsed_args.repo][0] if repo['id'] == parsed_args.repo][0]
init_path = "/clusters/{0}/git-repos/{1}/init" init_path = "/clusters/{0}/git-repos/{1}/init"
result = APIClient.put_request(init_path.format(env, repo_id), {}) APIClient.put_request(init_path.format(env, repo_id), {})
return (self.columns, {}) return (self.columns, {})
@ -250,8 +263,8 @@ class DownloadConfgs(command.Command):
for repo in repos: for repo in repos:
key_path = os.path.join( key_path = os.path.join(
parsed_args.repo_dir, parsed_args.repo_dir,
repo['repo_name'] + '.key') repo['repo_name'] + '.key')
with open(key_path, 'w') as keyf: with open(key_path, 'w') as keyf:
keyf.write(repo['user_key']) keyf.write(repo['user_key'])
os.chmod(key_path, 0o600) os.chmod(key_path, 0o600)
@ -283,10 +296,10 @@ class DownloadConfgs(command.Command):
for params in ext_settings['resource_mapping'].values(): for params in ext_settings['resource_mapping'].values():
path = params['path'] path = params['path']
target_path = os.path.join( target_path = os.path.join(
repo_path, repo_path,
"node_{}_configs".format(node['id']), "node_{}_configs".format(node['id']),
os.path.basename(path) os.path.basename(path)
) )
with fabric.api.settings( with fabric.api.settings(
host_string=node['ip'], host_string=node['ip'],
key_filename=key, key_filename=key,
@ -305,6 +318,6 @@ class DownloadConfgs(command.Command):
gitrepo.git.commit('-m "Configs updated"') gitrepo.git.commit('-m "Configs updated"')
with gitrepo.git.custom_environment(GIT_SSH_COMMAND=ssh_cmd): with gitrepo.git.custom_environment(GIT_SSH_COMMAND=ssh_cmd):
push_result = gitrepo.remotes.origin.\ push_result = gitrepo.remotes.origin.\
push(refspec='HEAD:' + cfg_branch) push(refspec='HEAD:' + cfg_branch)
print("Push result {}".format(push_result)) print("Push result {}".format(push_result))
return ((), {}) return ((), {})

View File

@ -1,12 +1,26 @@
from nailgun import objects # Licensed under the Apache License, Version 2.0 (the "License"); you may
from nailgun.errors import errors # not use this file except in compliance with the License. You may obtain
from nailgun.api.v1.validators import base # a copy of the License at
from nailgun.api.v1.handlers.base import content #
from nailgun.api.v1.handlers.base import \ # http://www.apache.org/licenses/LICENSE-2.0
SingleHandler, CollectionHandler, BaseHandler #
# 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.
from fuel_external_git.objects import GitRepo, GitRepoCollection
from fuel_external_git import json_schema from fuel_external_git import json_schema
from fuel_external_git.objects import GitRepo
from fuel_external_git.objects import GitRepoCollection
from nailgun.api.v1.handlers.base import BaseHandler
from nailgun.api.v1.handlers.base import CollectionHandler
from nailgun.api.v1.handlers.base import content
from nailgun.api.v1.handlers.base import SingleHandler
from nailgun.api.v1.validators import base
from nailgun.errors import errors
from nailgun import objects
REPOS_DIR = '/var/lib/fuel_repos' REPOS_DIR = '/var/lib/fuel_repos'

View File

@ -1,3 +1,15 @@
# 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.
single_schema = { single_schema = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"title": "Cluster", "title": "Cluster",

View File

@ -16,19 +16,18 @@ import logging.config
from alembic import context from alembic import context
import sqlalchemy import sqlalchemy
#from tuning_box import db
config = context.config config = context.config
if config.get_main_option('table_prefix') is None: if config.get_main_option('table_prefix') is None:
config.set_main_option('table_prefix', '') config.set_main_option('table_prefix', '')
if config.config_file_name: if config.config_file_name:
logging.config.fileConfig(config.config_file_name) logging.config.fileConfig(config.config_file_name)
#target_metadata = db.db.metadata
target_metadata = None target_metadata = None
def run_migrations_offline(): def run_migrations_offline():
"""Run migrations in 'offline' mode. """Run migrations in 'offline' mode.
This configures the context with just a URL This configures the context with just a URL
and not an Engine, though an Engine is acceptable and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation here as well. By skipping the Engine creation
@ -48,6 +47,7 @@ def run_migrations_offline():
def run_migrations_online(): def run_migrations_online():
"""Run migrations in 'online' mode. """Run migrations in 'online' mode.
In this scenario we need to create an Engine In this scenario we need to create an Engine
and associate a connection with the context. and associate a connection with the context.
""" """

View File

@ -1,3 +1,15 @@
# 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.
"""change_constrains """change_constrains
Revision ID: d59114c46ac4 Revision ID: d59114c46ac4
@ -13,6 +25,7 @@ branch_labels = None
depends_on = None depends_on = None
import sqlalchemy as sa import sqlalchemy as sa
from alembic import context from alembic import context
from alembic import op from alembic import op

View File

@ -1,3 +1,15 @@
# 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.
"""Init """Init
Revision ID: e3b840e64e53 Revision ID: e3b840e64e53
@ -13,9 +25,9 @@ branch_labels = None
depends_on = None depends_on = None
import sqlalchemy as sa import sqlalchemy as sa
from alembic import context from alembic import context
from alembic import op from alembic import op
from sqlalchemy.dialects import postgresql as psql
def upgrade(): def upgrade():

View File

@ -1,3 +1,15 @@
# 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.
from sqlalchemy import Column from sqlalchemy import Column
from sqlalchemy import Integer from sqlalchemy import Integer
from sqlalchemy import String from sqlalchemy import String

View File

@ -1,19 +1,34 @@
# 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 os import os
import shutil import shutil
import yaml import yaml
from distutils.dir_util import copy_tree from distutils.dir_util import copy_tree
from nailgun.db import db
from nailgun.objects import NailgunObject, NailgunCollection, Cluster
from nailgun.objects.serializers.base import BasicSerializer
from nailgun.logger import logger
from nailgun.errors import errors
from git import Repo
from git import exc
from fuel_external_git.models import GitRepo
from fuel_external_git import const from fuel_external_git import const
from fuel_external_git.models import GitRepo
from git import exc
from git import Repo
from nailgun.db import db
from nailgun.errors import errors
from nailgun.logger import logger
from nailgun.objects import Cluster
from nailgun.objects import NailgunCollection
from nailgun.objects import NailgunObject
from nailgun.objects.serializers.base import BasicSerializer
class GitRepoSerializer(BasicSerializer): class GitRepoSerializer(BasicSerializer):
@ -34,8 +49,7 @@ class GitRepo(NailgunObject):
@classmethod @classmethod
def get_by_cluster_id(self, cluster_id): def get_by_cluster_id(self, cluster_id):
instance = db().query(self.model).\ instance = db().query(self.model).\
filter(self.model.env_id == cluster_id).\ filter(self.model.env_id == cluster_id).first()
first()
if instance is not None: if instance is not None:
try: try:
instance.repo = Repo(os.path.join(const.REPOS_DIR, instance.repo = Repo(os.path.join(const.REPOS_DIR,
@ -92,8 +106,8 @@ class GitRepo(NailgunObject):
@classmethod @classmethod
def init(self, instance): def init(self, instance):
overrides = { overrides = {
'nodes': {}, 'nodes': {},
'roles': {} 'roles': {}
} }
repo_path = os.path.join(const.REPOS_DIR, instance.repo_name) repo_path = os.path.join(const.REPOS_DIR, instance.repo_name)
templates_dir = os.path.join(os.path.dirname(__file__), templates_dir = os.path.join(os.path.dirname(__file__),
@ -102,7 +116,7 @@ class GitRepo(NailgunObject):
try: try:
self.checkout(instance) self.checkout(instance)
except exc.GitCommandError, e: except exc.GitCommandError as e:
logger.debug(("Remote returned following error {}. " logger.debug(("Remote returned following error {}. "
"Seem remote has not been initialised. " "Seem remote has not been initialised. "
"Skipping checkout".format(e))) "Skipping checkout".format(e)))
@ -129,7 +143,7 @@ class GitRepo(NailgunObject):
with instance.repo.git.custom_environment( with instance.repo.git.custom_environment(
GIT_SSH_COMMAND=ssh_cmd): GIT_SSH_COMMAND=ssh_cmd):
res = instance.repo.remotes.origin.push( res = instance.repo.remotes.origin.push(
refspec='HEAD:' + instance.ref) refspec='HEAD:' + instance.ref)
logger.debug("Push result {}".format(res[0].flags)) logger.debug("Push result {}".format(res[0].flags))
if res[0].flags not in (2, 256): if res[0].flags not in (2, 256):
logger.debug("Push error. Result code should be 2 or 256") logger.debug("Push error. Result code should be 2 or 256")

View File

@ -1,12 +1,23 @@
import os # 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 ConfigParser import ConfigParser
import os
from nailgun.logger import logger from nailgun.logger import logger
class OpenStackConfig(object): class OpenStackConfig(object):
def __init__(self, config_file, resource_name=None): def __init__(self, config_file, resource_name=None):
cf_basename = os.path.basename(config_file)
self.config = ConfigParser.ConfigParser() self.config = ConfigParser.ConfigParser()
self.config.read(config_file) self.config.read(config_file)
if resource_name: if resource_name:
@ -18,7 +29,9 @@ class OpenStackConfig(object):
format(config_file, self.config_name)) format(config_file, self.config_name))
def to_config_dict(self): def to_config_dict(self):
"""Function returns OpenStack config file in dictionary form """Config transformation
Function returns OpenStack config file in dictionary form
compatible with override_configuration resource in compatible with override_configuration resource in
fuel-library fuel-library
Example: Example:

View File

@ -1,3 +1,16 @@
# 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.
# tst
import os import os
import yaml import yaml

View File

@ -1,8 +1,20 @@
#!/bin/env python #!/bin/env python
# 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 argparse
import ConfigParser
import os import os
import sys import sys
import ConfigParser
import argparse
import yaml import yaml

View File

@ -1,3 +1,15 @@
# 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 os import os
from oslotest import base from oslotest import base

View File

@ -1,7 +1,19 @@
# 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 os import os
from fuel_external_git.tests import base
from fuel_external_git.openstack_config import OpenStackConfig from fuel_external_git.openstack_config import OpenStackConfig
from fuel_external_git.tests import base
class TestOpenStackConfig(base.TestCase): class TestOpenStackConfig(base.TestCase):

View File

@ -1,3 +1,15 @@
# 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 copy import copy
from fuel_external_git.tests import base from fuel_external_git.tests import base
from fuel_external_git import utils from fuel_external_git import utils
@ -23,7 +35,7 @@ class TestUtils(base.TestCase):
b = {} b = {}
utils.deep_merge(a, b) utils.deep_merge(a, b)
self.assertEqual(a, {}) self.assertEqual(a, {})
def test_deep_merge_one_empy(self): def test_deep_merge_one_empy(self):
sample_dict = { sample_dict = {
'a': {'b': {'c': 'd'}}, 'a': {'b': {'c': 'd'}},
@ -32,7 +44,7 @@ class TestUtils(base.TestCase):
new_dict = copy.deepcopy(sample_dict) new_dict = copy.deepcopy(sample_dict)
utils.deep_merge(new_dict, {}) utils.deep_merge(new_dict, {})
self.assertEqual(new_dict, sample_dict) self.assertEqual(new_dict, sample_dict)
new_dict = {} new_dict = {}
utils.deep_merge(new_dict, sample_dict) utils.deep_merge(new_dict, sample_dict)
self.assertEqual(new_dict, sample_dict) self.assertEqual(new_dict, sample_dict)
@ -47,13 +59,13 @@ class TestUtils(base.TestCase):
'x': {'b': {'c': 'd'}}, 'x': {'b': {'c': 'd'}},
'y': {'f': {'g': 'h'}}, 'y': {'f': {'g': 'h'}},
} }
result = { result = {
'a': {'b': {'c': 'd'}}, 'a': {'b': {'c': 'd'}},
'e': {'f': {'g': 'h'}}, 'e': {'f': {'g': 'h'}},
'x': {'b': {'c': 'd'}}, 'x': {'b': {'c': 'd'}},
'y': {'f': {'g': 'h'}}, 'y': {'f': {'g': 'h'}},
} }
utils.deep_merge(a, b) utils.deep_merge(a, b)
self.assertEqual(a, result) self.assertEqual(a, result)

View File

@ -1,3 +1,15 @@
# 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 os import os
from oslo_utils import importutils from oslo_utils import importutils
@ -6,8 +18,6 @@ from nailgun.db.sqlalchemy.models import Cluster
from nailgun.db.sqlalchemy.models import Release from nailgun.db.sqlalchemy.models import Release
from nailgun.logger import logger from nailgun.logger import logger
from fuel_external_git.openstack_config import OpenStackConfig
def get_file_exts_list(resource_mapping): def get_file_exts_list(resource_mapping):
res = set() res = set()
@ -27,7 +37,7 @@ def get_config_hash(file_dir, resource_mapping, exts=['conf']):
if conf.split('.')[-1] in exts] if conf.split('.')[-1] in exts]
for conf_file in conf_files: for conf_file in conf_files:
resource_name = None resource_name = None
driver_str = 'fuel_external_git.openstack_config.OpenStackConfig' driver_str = 'fuel_external_git.openstack_config.OpenStackConfig'
for resource, params in resource_mapping.items(): for resource, params in resource_mapping.items():
if params['alias'] == conf_file: if params['alias'] == conf_file:
resource_name = resource resource_name = resource
@ -47,7 +57,7 @@ def deep_merge(dct, merge_dct):
dct[k] = merge_dct[k] dct[k] = merge_dct[k]
# TODO (dukov) Remove this ugly staff once extension management is available # TODO(dukov) Remove this ugly staff once extension management is available
def register_extension(ext_name): def register_extension(ext_name):
def decorator(cls): def decorator(cls):
exts = {cl: cl['extensions'] for cl in exts = {cl: cl['extensions'] for cl in