shellfoundry 1.2.25__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. shellfoundry/__init__.py +8 -0
  2. shellfoundry/__main__.py +7 -0
  3. shellfoundry/bootstrap.py +169 -0
  4. shellfoundry/commands/__init__.py +0 -0
  5. shellfoundry/commands/config_command.py +87 -0
  6. shellfoundry/commands/delete_command.py +24 -0
  7. shellfoundry/commands/dist_command.py +34 -0
  8. shellfoundry/commands/extend_command.py +182 -0
  9. shellfoundry/commands/generate_command.py +44 -0
  10. shellfoundry/commands/get_templates_command.py +151 -0
  11. shellfoundry/commands/install_command.py +81 -0
  12. shellfoundry/commands/list_command.py +110 -0
  13. shellfoundry/commands/new_command.py +393 -0
  14. shellfoundry/commands/pack_command.py +45 -0
  15. shellfoundry/commands/show_command.py +55 -0
  16. shellfoundry/data/standards.json +44 -0
  17. shellfoundry/data/templates.yml +117 -0
  18. shellfoundry/decorators/__init__.py +1 -0
  19. shellfoundry/decorators/standards.py +20 -0
  20. shellfoundry/decorators/version_check.py +52 -0
  21. shellfoundry/exceptions.py +52 -0
  22. shellfoundry/models/__init__.py +0 -0
  23. shellfoundry/models/install_config.py +82 -0
  24. shellfoundry/models/shell_template.py +22 -0
  25. shellfoundry/models/shellfoundry_settings.py +13 -0
  26. shellfoundry/utilities/__init__.py +114 -0
  27. shellfoundry/utilities/archive_creator.py +35 -0
  28. shellfoundry/utilities/cloudshell_api/__init__.py +1 -0
  29. shellfoundry/utilities/cloudshell_api/client_wrapper.py +71 -0
  30. shellfoundry/utilities/config/__init__.py +0 -0
  31. shellfoundry/utilities/config/config_context.py +44 -0
  32. shellfoundry/utilities/config/config_file_creation.py +32 -0
  33. shellfoundry/utilities/config/config_providers.py +50 -0
  34. shellfoundry/utilities/config/config_record.py +24 -0
  35. shellfoundry/utilities/config_reader.py +154 -0
  36. shellfoundry/utilities/constants.py +37 -0
  37. shellfoundry/utilities/cookiecutter_integration.py +67 -0
  38. shellfoundry/utilities/driver_generator.py +121 -0
  39. shellfoundry/utilities/filters.py +45 -0
  40. shellfoundry/utilities/installer.py +44 -0
  41. shellfoundry/utilities/modifiers/__init__.py +0 -0
  42. shellfoundry/utilities/modifiers/configuration/__init__.py +0 -0
  43. shellfoundry/utilities/modifiers/configuration/aggregated_modifiers.py +26 -0
  44. shellfoundry/utilities/modifiers/configuration/password_modification.py +48 -0
  45. shellfoundry/utilities/modifiers/definition/__init__.py +0 -0
  46. shellfoundry/utilities/modifiers/definition/definition_modification.py +205 -0
  47. shellfoundry/utilities/package_builder.py +152 -0
  48. shellfoundry/utilities/python_dependencies_packager.py +53 -0
  49. shellfoundry/utilities/repository_downloader.py +76 -0
  50. shellfoundry/utilities/shell_config_reader.py +59 -0
  51. shellfoundry/utilities/shell_datamodel_merger.py +44 -0
  52. shellfoundry/utilities/shell_package.py +59 -0
  53. shellfoundry/utilities/shell_package_builder.py +149 -0
  54. shellfoundry/utilities/shell_package_installer.py +231 -0
  55. shellfoundry/utilities/standards/__init__.py +3 -0
  56. shellfoundry/utilities/standards/consts.py +2 -0
  57. shellfoundry/utilities/standards/standards_retriever.py +32 -0
  58. shellfoundry/utilities/standards/standards_versions.py +34 -0
  59. shellfoundry/utilities/temp_dir_context.py +17 -0
  60. shellfoundry/utilities/template_retriever.py +309 -0
  61. shellfoundry/utilities/template_url.py +38 -0
  62. shellfoundry/utilities/template_versions.py +47 -0
  63. shellfoundry/utilities/validations/__init__.py +2 -0
  64. shellfoundry/utilities/validations/shell_generation_validation.py +29 -0
  65. shellfoundry/utilities/validations/shell_name_validations.py +10 -0
  66. shellfoundry/utilities/version_utilities.py +19 -0
  67. shellfoundry-1.2.25.dist-info/LICENSE +201 -0
  68. shellfoundry-1.2.25.dist-info/METADATA +417 -0
  69. shellfoundry-1.2.25.dist-info/RECORD +72 -0
  70. shellfoundry-1.2.25.dist-info/WHEEL +5 -0
  71. shellfoundry-1.2.25.dist-info/entry_points.txt +2 -0
  72. shellfoundry-1.2.25.dist-info/top_level.txt +1 -0
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ from io import open
6
+
7
+ import yaml
8
+
9
+ from shellfoundry.models.install_config import (
10
+ DEFAULT_AUTHOR,
11
+ DEFAULT_DOMAIN,
12
+ DEFAULT_GITHUB_LOGIN,
13
+ DEFAULT_GITHUB_PASSWORD,
14
+ DEFAULT_HOST,
15
+ DEFAULT_ONLINE_MODE,
16
+ DEFAULT_PASSWORD,
17
+ DEFAULT_PORT,
18
+ DEFAULT_TEMPLATE_LOCATION,
19
+ DEFAULT_USERNAME,
20
+ InstallConfig,
21
+ )
22
+ from shellfoundry.models.shellfoundry_settings import (
23
+ DEFAULT_DEFAULT_VIEW,
24
+ ShellFoundrySettings,
25
+ )
26
+ from shellfoundry.utilities.config.config_providers import DefaultConfigProvider
27
+
28
+ INSTALL = "install"
29
+
30
+ HOST = "host"
31
+ PORT = "port"
32
+ USERNAME = "username"
33
+ PASSWORD = "password"
34
+ DOMAIN = "domain"
35
+ AUTHOR = "author"
36
+ ONLINE_MODE = "online_mode"
37
+ TEMPLATE_LOCATION = "template_location"
38
+ GITHUB_LOGIN = "github_login"
39
+ GITHUB_PASSWORD = "github_password"
40
+
41
+ DEFAULT_VIEW = "defaultview"
42
+
43
+
44
+ def get_with_default(install_config, parameter_name, default_value):
45
+ """Get configuration with default values.
46
+
47
+ :param install_config: A dict represents the install section inside the configuration file # noqa: E501
48
+ :param parameter_name: Specific key inside the install section
49
+ :param default_value: Default value in cases that the key cannot be found
50
+ :return: The value of the key in the configuration file or default value if key cannot be found # noqa: E501
51
+ """
52
+ return (
53
+ install_config[parameter_name]
54
+ if install_config and parameter_name in install_config
55
+ else default_value
56
+ )
57
+
58
+
59
+ class Configuration(object):
60
+ def __init__(self, reader, config_provider=None):
61
+ self.reader = reader
62
+ self.config_provider = config_provider or DefaultConfigProvider()
63
+
64
+ def read(self):
65
+ config_path = self.config_provider.get_config_path()
66
+
67
+ if config_path is None or not os.path.isfile(config_path):
68
+ return self.reader.get_defaults()
69
+
70
+ with open(config_path) as stream:
71
+ config = yaml.safe_load(stream.read())
72
+
73
+ if not config or INSTALL not in config:
74
+ return self.reader.get_defaults()
75
+
76
+ return self.reader.read_from_config(config[INSTALL])
77
+
78
+ @staticmethod
79
+ def readall(config_path, mark_defaults=None):
80
+ """Reads configuration from given file.
81
+
82
+ Missing keys will be filled with their defaults.
83
+ """
84
+ config_data = None
85
+ if os.path.exists(config_path):
86
+ with open(config_path, mode="r", encoding="utf8") as conf_file:
87
+ config_data = yaml.safe_load(conf_file)
88
+
89
+ if not config_data or INSTALL not in config_data:
90
+ config_data = {INSTALL: {}}
91
+
92
+ mark_defaults_f = Configuration._mark_defaults
93
+ install_cfg_def = {
94
+ (k, mark_defaults_f(v, mark_defaults))
95
+ for k, v in InstallConfig.get_default().__dict__.items()
96
+ }
97
+ sf_cfg_def = {
98
+ (k, mark_defaults_f(v, mark_defaults))
99
+ for k, v in ShellFoundrySettings.get_default().__dict__.items()
100
+ }
101
+ all_cfg = dict(install_cfg_def)
102
+ all_cfg.update(sf_cfg_def)
103
+ all_cfg.update(config_data[INSTALL])
104
+ return {INSTALL: all_cfg}
105
+
106
+ @staticmethod
107
+ def _mark_defaults(value, mark_defaults_char):
108
+ if not mark_defaults_char:
109
+ return str(value)
110
+ return "{value} {default_char}".format(
111
+ value=str(value), default_char=mark_defaults_char
112
+ )
113
+
114
+
115
+ class CloudShellConfigReader(object):
116
+ def get_defaults(self):
117
+ return InstallConfig.get_default()
118
+
119
+ def read_from_config(self, config):
120
+ host = get_with_default(config, HOST, DEFAULT_HOST)
121
+ port = get_with_default(config, PORT, DEFAULT_PORT)
122
+ username = get_with_default(config, USERNAME, DEFAULT_USERNAME)
123
+ password = get_with_default(config, PASSWORD, DEFAULT_PASSWORD)
124
+ domain = get_with_default(config, DOMAIN, DEFAULT_DOMAIN)
125
+ author = get_with_default(config, AUTHOR, DEFAULT_AUTHOR)
126
+ online_mode = get_with_default(config, ONLINE_MODE, DEFAULT_ONLINE_MODE)
127
+ template_location = get_with_default(
128
+ config, TEMPLATE_LOCATION, DEFAULT_TEMPLATE_LOCATION
129
+ )
130
+ github_login = get_with_default(config, GITHUB_LOGIN, DEFAULT_GITHUB_LOGIN)
131
+ github_password = get_with_default(
132
+ config, GITHUB_PASSWORD, DEFAULT_GITHUB_PASSWORD
133
+ )
134
+ return InstallConfig(
135
+ host,
136
+ port,
137
+ username,
138
+ password,
139
+ domain,
140
+ author,
141
+ online_mode,
142
+ template_location,
143
+ github_login,
144
+ github_password,
145
+ )
146
+
147
+
148
+ class ShellFoundryConfig(object):
149
+ def get_defaults(self):
150
+ return ShellFoundrySettings.get_default()
151
+
152
+ def read_from_config(self, config):
153
+ defaultview = get_with_default(config, DEFAULT_VIEW, DEFAULT_DEFAULT_VIEW)
154
+ return ShellFoundrySettings(defaultview)
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+
6
+ __all__ = [
7
+ "TOSCA_META_LOCATION",
8
+ "TEMPLATE_AUTHOR_FIELD",
9
+ "TEMPLATE_VERSION",
10
+ "TEMPLATE_BASED_ON",
11
+ "CLOUDSHELL_MAX_RETRIES",
12
+ "CLOUDSHELL_RETRY_INTERVAL_SEC",
13
+ "DEFAULT_TIME_WAIT",
14
+ "TEMPLATE_PROPERTY",
15
+ "TEMPLATES_YML",
16
+ "SERVER_VERSION_KEY",
17
+ ]
18
+
19
+ CLOUDSHELL_MAX_RETRIES = 5
20
+ CLOUDSHELL_RETRY_INTERVAL_SEC = 0.5
21
+ DEFAULT_TIME_WAIT = 1.0
22
+ METADATA_AUTHOR_FIELD = "Created-By"
23
+ TEMPLATE_AUTHOR_FIELD = "metadata/template_author"
24
+ TEMPLATE_VERSION = "metadata/template_version"
25
+ TEMPLATE_BASED_ON = "metadata/template_based_on"
26
+ TEMPLATE_INFO_FILE = "cookiecutter.json"
27
+ TEMPLATE_PROPERTY = {
28
+ "type": "string",
29
+ "default": "fast",
30
+ "description": "Some attribute description",
31
+ "constraints": [{"valid_values": ["fast", "slow"]}],
32
+ }
33
+ TEMPLATES_YML = (
34
+ "https://raw.github.com/QualiSystems/shellfoundry/master/templates_v1.yml"
35
+ )
36
+ TOSCA_META_LOCATION = os.path.join("TOSCA-Metadata", "TOSCA.meta")
37
+ SERVER_VERSION_KEY = "server_version"
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import datetime
5
+ import os
6
+
7
+ from click import ClickException
8
+ from cookiecutter.exceptions import OutputDirExistsException
9
+ from cookiecutter.main import cookiecutter
10
+
11
+ from shellfoundry.utilities.config_reader import CloudShellConfigReader, Configuration
12
+ from shellfoundry.utilities.constants import TEMPLATE_INFO_FILE
13
+
14
+
15
+ class CookiecutterTemplateCompiler(object):
16
+ def __init__(self):
17
+ self.cloudshell_config_reader = Configuration(CloudShellConfigReader())
18
+
19
+ def compile_template(
20
+ self,
21
+ shell_name,
22
+ template_path,
23
+ extra_context,
24
+ running_on_same_folder,
25
+ python_version=None,
26
+ ):
27
+
28
+ if python_version is None:
29
+ python_version = ""
30
+ else:
31
+ python_version = ' PythonVersion="{}"'.format(str(python_version))
32
+
33
+ extra_context.update(
34
+ {
35
+ "project_name": shell_name,
36
+ "full_name": self.cloudshell_config_reader.read().author,
37
+ "release_date": datetime.datetime.now().strftime("%B %Y"),
38
+ "python_version": str(python_version),
39
+ }
40
+ )
41
+
42
+ if running_on_same_folder:
43
+ output_dir = os.path.pardir
44
+ else:
45
+ output_dir = os.path.curdir
46
+
47
+ try:
48
+ cookiecutter(
49
+ template_path,
50
+ no_input=True,
51
+ extra_context=extra_context,
52
+ overwrite_if_exists=False,
53
+ output_dir=output_dir,
54
+ )
55
+ except OutputDirExistsException as err:
56
+ if str(err).startswith("Error: "):
57
+ msg = str(err)[6:].strip()
58
+ else:
59
+ msg = str(err)
60
+ raise ClickException(msg)
61
+
62
+ @staticmethod
63
+ def _remove_template_info_file(shell_path):
64
+ """Remove template info file from shell structure."""
65
+ template_info_file_path = os.path.join(shell_path, TEMPLATE_INFO_FILE)
66
+ if os.path.exists(template_info_file_path):
67
+ os.remove(template_info_file_path)
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/python
2
+ try:
3
+ from urllib.error import URLError
4
+ except ImportError:
5
+ from urllib2 import URLError
6
+
7
+ import zipfile
8
+ from io import open
9
+ from os import path
10
+
11
+ import click
12
+ from cloudshell.rest.api import PackagingRestApiClient
13
+ from requests import post
14
+
15
+ from shellfoundry.utilities.temp_dir_context import TempDirContext
16
+
17
+
18
+ class DriverGenerator(object):
19
+ def generate_driver(
20
+ self,
21
+ cloudshell_config,
22
+ destination_path,
23
+ package_full_path,
24
+ shell_filename,
25
+ shell_name,
26
+ ):
27
+ """Generates Python data model by connecting to Cloudshell server.
28
+
29
+ :param cloudshell_config:
30
+ :type cloudshell_config: InstallConfig
31
+ :param destination_path:
32
+ :param package_full_path:
33
+ :param shell_filename:
34
+ :param shell_name:
35
+ :return:
36
+ """
37
+ client = self._connect_to_cloudshell(cloudshell_config)
38
+ self._generate_driver_data_model(
39
+ client=client,
40
+ cloudshell_config=cloudshell_config,
41
+ destination_path=destination_path,
42
+ package_full_path=package_full_path,
43
+ shell_filename=shell_filename,
44
+ shell_name=shell_name,
45
+ )
46
+
47
+ @staticmethod
48
+ def _generate_driver_data_model(
49
+ client,
50
+ cloudshell_config,
51
+ destination_path,
52
+ package_full_path,
53
+ shell_filename,
54
+ shell_name,
55
+ ):
56
+ """Generates driver data model.
57
+
58
+ :param client:
59
+ :param cloudshell_config:
60
+ :type cloudshell_config: InstallConfig
61
+ :param destination_path:
62
+ :param package_full_path:
63
+ :param shell_filename:
64
+ :param shell_name:
65
+ :return:
66
+ """
67
+ url = "http://{}:{}/API/ShellDrivers/Generate".format(
68
+ cloudshell_config.host, cloudshell_config.port
69
+ )
70
+ token = client.token
71
+ response = post(
72
+ url,
73
+ files={path.basename(shell_filename): open(package_full_path, "rb")},
74
+ headers={"Authorization": "Basic " + token},
75
+ )
76
+
77
+ if response.status_code != 200:
78
+ error_message = "Code generation failed with code {} and error {}".format(
79
+ response.status_code, response.text
80
+ )
81
+ click.echo(message=error_message, err=True)
82
+ return
83
+
84
+ click.echo("Extracting data model ...")
85
+ with TempDirContext(remove_dir_on_error=False, prefix=shell_name) as temp_dir:
86
+ generated_zip = path.join(temp_dir, shell_filename)
87
+ click.echo("Writing temporary file {}".format(generated_zip))
88
+ with open(generated_zip, "wb") as driver_file:
89
+ driver_file.write(response.content)
90
+
91
+ click.echo("Extracting generated code at {}".format(destination_path))
92
+ with zipfile.ZipFile(generated_zip) as zf:
93
+ zf.extractall(destination_path)
94
+
95
+ @staticmethod
96
+ def _connect_to_cloudshell(cloudshell_config):
97
+ try:
98
+ try:
99
+ client = PackagingRestApiClient.login(
100
+ host=cloudshell_config.host,
101
+ port=cloudshell_config.port,
102
+ username=cloudshell_config.username,
103
+ password=cloudshell_config.password,
104
+ domain=cloudshell_config.domain,
105
+ )
106
+ return client
107
+ except AttributeError:
108
+ client = PackagingRestApiClient(
109
+ ip=cloudshell_config.host,
110
+ port=cloudshell_config.port,
111
+ username=cloudshell_config.username,
112
+ password=cloudshell_config.password,
113
+ domain=cloudshell_config.domain,
114
+ )
115
+ return client
116
+ except URLError:
117
+ click.echo(
118
+ "Login to CloudShell failed. Please verify the credentials in cloudshell_config.yml", # noqa: E501
119
+ err=True,
120
+ )
121
+ raise
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ from . import (
4
+ GEN_ONE,
5
+ GEN_ONE_FILTER,
6
+ GEN_TWO,
7
+ GEN_TWO_FILTER,
8
+ LAYER_ONE,
9
+ LAYER_ONE_FILTER,
10
+ NO_FILTER,
11
+ )
12
+
13
+
14
+ class CompositeFilter(object):
15
+ def __init__(self, template_type=None):
16
+ self.template_type = template_type or NO_FILTER
17
+ self.filters = {
18
+ GEN_ONE: GenOneFilter,
19
+ GEN_TWO: GenTwoFilter,
20
+ LAYER_ONE: LayerOneFilter,
21
+ NO_FILTER: NoFilter,
22
+ }
23
+
24
+ def filter(self, template_name): # noqa: A003
25
+ return self.filters.get(self.template_type, NoFilter)().filter(template_name)
26
+
27
+
28
+ class GenOneFilter(object):
29
+ def filter(self, template_name): # noqa: A003
30
+ return GEN_ONE_FILTER in template_name
31
+
32
+
33
+ class GenTwoFilter(object):
34
+ def filter(self, template_name): # noqa: A003
35
+ return GEN_TWO_FILTER in template_name
36
+
37
+
38
+ class LayerOneFilter(object):
39
+ def filter(self, template_name): # noqa: A003
40
+ return LAYER_ONE_FILTER in template_name
41
+
42
+
43
+ class NoFilter(object):
44
+ def filter(self, template_name): # noqa: A003
45
+ return True
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+
6
+ import click
7
+ from cloudshell.rest.api import PackagingRestApiClient
8
+
9
+
10
+ class ShellInstaller(object):
11
+ def install(self, package_name, config):
12
+ """Installs package according to cloudshell.
13
+
14
+ :param package_name: Package name to install
15
+ :type package_name str
16
+ :param config: Configuration to be used for
17
+ :type config shellfoundry.models.install_config.InstallConfig
18
+ :return:
19
+ """
20
+ package_full_path = os.path.join(os.getcwd(), "dist", package_name + ".zip")
21
+ click.echo(
22
+ "Installing package {} into CloudShell at http://{}:{}".format(
23
+ package_full_path, config.host, config.port
24
+ )
25
+ )
26
+
27
+ try:
28
+ client = PackagingRestApiClient.login(
29
+ host=config.host,
30
+ port=config.port,
31
+ username=config.username,
32
+ password=config.password,
33
+ domain=config.domain,
34
+ )
35
+ except AttributeError:
36
+ client = PackagingRestApiClient(
37
+ ip=config.host,
38
+ port=config.port,
39
+ username=config.username,
40
+ password=config.password,
41
+ domain=config.domain,
42
+ )
43
+
44
+ client.import_package(package_full_path)
File without changes
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from .password_modification import PasswordModification
5
+
6
+ DEFAULT_MODIFIER = (
7
+ lambda x: x
8
+ ) # Default modifier returns the passed value unchanged # noqa: E731
9
+
10
+
11
+ class AggregatedModifiers(object):
12
+ def __init__(self, modifiers=None, default=None):
13
+ self.modifiers = modifiers or {
14
+ key: PasswordModification().modify
15
+ for key in PasswordModification.HANDLING_KEYS
16
+ }
17
+ self.default_modifier = default or AggregatedModifiers._default_modifier
18
+
19
+ def modify(self, key, value):
20
+ if key in self.modifiers:
21
+ return self.modifiers[key](value)
22
+ return self.default_modifier(value)
23
+
24
+ @staticmethod
25
+ def _default_modifier(value): # Default modifier returns the passed value unchanged
26
+ return value
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import base64
5
+ import binascii
6
+ import sys
7
+
8
+ import shellfoundry.exceptions as exceptions
9
+
10
+
11
+ class PasswordModification(object):
12
+ HANDLING_KEYS = ["password", "github_password"]
13
+
14
+ def modify(self, value):
15
+ encryption_key = self._get_encryption_key()
16
+ encoded = self._decode_encode(value, encryption_key)
17
+ if sys.version_info[0] < 3:
18
+ return base64.b64encode(encoded)
19
+ else:
20
+ return base64.b64encode(encoded.encode()).decode()
21
+
22
+ def normalize(self, value):
23
+ try:
24
+ encryption_key = self._get_encryption_key()
25
+ if sys.version_info[0] < 3:
26
+ decoded = self._decode_encode(
27
+ base64.decodestring(value), encryption_key
28
+ )
29
+ else:
30
+ decoded = self._decode_encode(
31
+ base64.decodebytes(value.encode()).decode(), encryption_key
32
+ )
33
+ return decoded
34
+ except binascii.Error:
35
+ return value
36
+
37
+ def _get_encryption_key(self):
38
+ from platform import node
39
+
40
+ machine_name = node()
41
+ if not machine_name:
42
+ raise exceptions.PlatformNameIsEmptyException()
43
+ return machine_name
44
+
45
+ def _decode_encode(self, value, key):
46
+ return "".join(
47
+ chr(ord(source) ^ ord(key)) for source, key in zip(value, key * 100)
48
+ )