easy-sm 0.1.3__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.

Potentially problematic release.


This version of easy-sm might be problematic. Click here for more details.

Files changed (36) hide show
  1. easy_sm/__init__.py +0 -0
  2. easy_sm/__main__.py +27 -0
  3. easy_sm/commands/__init__.py +0 -0
  4. easy_sm/commands/build.py +81 -0
  5. easy_sm/commands/cloud.py +356 -0
  6. easy_sm/commands/initialize.py +169 -0
  7. easy_sm/commands/local.py +151 -0
  8. easy_sm/commands/push.py +86 -0
  9. easy_sm/config/__init__.py +0 -0
  10. easy_sm/config/config.py +52 -0
  11. easy_sm/sagemaker/__init__.py +0 -0
  12. easy_sm/sagemaker/sagemaker.py +294 -0
  13. easy_sm/template/__init__.py +0 -0
  14. easy_sm/template/easy_sm_base/Dockerfile +27 -0
  15. easy_sm/template/easy_sm_base/__init__.py +0 -0
  16. easy_sm/template/easy_sm_base/build.sh +20 -0
  17. easy_sm/template/easy_sm_base/executor.sh +11 -0
  18. easy_sm/template/easy_sm_base/local_test/deploy_local.sh +7 -0
  19. easy_sm/template/easy_sm_base/local_test/process_local.sh +10 -0
  20. easy_sm/template/easy_sm_base/local_test/test_dir/input/data/training/.gitkeep +0 -0
  21. easy_sm/template/easy_sm_base/local_test/test_dir/model/.gitkeep +0 -0
  22. easy_sm/template/easy_sm_base/local_test/test_dir/output/.gitkeep +0 -0
  23. easy_sm/template/easy_sm_base/local_test/train_local.sh +7 -0
  24. easy_sm/template/easy_sm_base/prediction/__init__.py +0 -0
  25. easy_sm/template/easy_sm_base/prediction/serve +57 -0
  26. easy_sm/template/easy_sm_base/processing/__init__.py +0 -0
  27. easy_sm/template/easy_sm_base/push.sh +74 -0
  28. easy_sm/template/easy_sm_base/training/__init__.py +0 -0
  29. easy_sm/template/easy_sm_base/training/train +83 -0
  30. easy_sm/template/easy_sm_base/training/training.py +10 -0
  31. easy_sm-0.1.3.dist-info/LICENSE +21 -0
  32. easy_sm-0.1.3.dist-info/METADATA +17 -0
  33. easy_sm-0.1.3.dist-info/RECORD +36 -0
  34. easy_sm-0.1.3.dist-info/WHEEL +5 -0
  35. easy_sm-0.1.3.dist-info/entry_points.txt +2 -0
  36. easy_sm-0.1.3.dist-info/top_level.txt +1 -0
easy_sm/__init__.py ADDED
File without changes
easy_sm/__main__.py ADDED
@@ -0,0 +1,27 @@
1
+ import click
2
+ from easy_sm.commands.local import local
3
+ from easy_sm.commands.initialize import init
4
+ from easy_sm.commands.build import build
5
+ from easy_sm.commands.push import push
6
+ from easy_sm.commands.cloud import cloud
7
+
8
+ @click.group()
9
+ @click.option(u"-t", u"--docker-tag", default=u"latest", help=u"Specify tag for Docker image")
10
+ @click.pass_context
11
+ def cli(ctx, docker_tag):
12
+ """
13
+ easy_sm enables training and deploying machine learning models on AWS SageMaker in a few minutes!
14
+ """
15
+ ctx.obj = {'docker_tag': docker_tag}
16
+
17
+
18
+ def add_commands(cli):
19
+ cli.add_command(init)
20
+ cli.add_command(build)
21
+ cli.add_command(local)
22
+ cli.add_command(push)
23
+ cli.add_command(cloud)
24
+ # cli.add_command(configure)
25
+
26
+
27
+ add_commands(cli)
File without changes
@@ -0,0 +1,81 @@
1
+ import sys
2
+ import click
3
+ import os
4
+ import subprocess
5
+ from easy_sm.config.config import ConfigManager
6
+
7
+ def _config(app_name):
8
+ config_file_path = os.path.join(f'{app_name}.json')
9
+ if not os.path.isfile(config_file_path):
10
+ raise ValueError("This is not a easy_sm directory: {}".format(os.getcwd()))
11
+ else:
12
+ return ConfigManager(config_file_path).get_config()
13
+
14
+
15
+ def _build(source_dir, requirements_dir, image_name, docker_tag, python_version):
16
+ """
17
+ Builds a Docker image that contains code under the given source root directory.
18
+
19
+ Assumes that Docker is installed and running locally.
20
+
21
+ :param source_dir: [str], source root directory
22
+ :param requirements_dir: [str], path to requirements.txt
23
+ :param image_name: [str], The name of the Docker image
24
+ :param docker_tag: [str], the Docker tag for the image
25
+ """
26
+ easy_sm_module_path = os.path.relpath(os.path.join(source_dir, 'easy_sm_base/'))
27
+
28
+ build_script_path = os.path.join(easy_sm_module_path, 'build.sh')
29
+ dockerfile_path = os.path.join(easy_sm_module_path, 'Dockerfile')
30
+
31
+ train_file_path = os.path.join(easy_sm_module_path, 'training', 'train')
32
+ serve_file_path = os.path.join(easy_sm_module_path, 'prediction', 'serve')
33
+ executor_file_path = os.path.join(easy_sm_module_path, 'executor.sh')
34
+
35
+ if not os.path.isfile(build_script_path) or not os.path.isfile(train_file_path) or not \
36
+ os.path.isfile(serve_file_path):
37
+ raise ValueError("This is not a easy_sm directory: {}".format(source_dir))
38
+
39
+ os.chmod(train_file_path, 0o777)
40
+ os.chmod(serve_file_path, 0o777)
41
+ os.chmod(executor_file_path, 0o777)
42
+
43
+ target_dir_name = os.path.basename(os.path.normpath(source_dir))
44
+ output = subprocess.check_output(
45
+ [
46
+ "{}".format(build_script_path),
47
+ "{}".format(os.path.relpath(source_dir)),
48
+ "{}".format(os.path.relpath(target_dir_name)),
49
+ "{}".format(dockerfile_path),
50
+ "{}".format(os.path.relpath(requirements_dir)),
51
+ docker_tag,
52
+ image_name,
53
+ python_version
54
+ ]
55
+ )
56
+ print(output)
57
+
58
+
59
+ @click.command()
60
+ @click.option(
61
+ u"-a",
62
+ u"--app-name",
63
+ required=True,
64
+ help="The app name whose json file will be referenced for setting up command"
65
+ )
66
+ @click.pass_obj
67
+ def build(obj, app_name):
68
+ """
69
+ Command to build SageMaker app
70
+ """
71
+ print("Started building SageMaker Docker image. It will take some minutes...\n")
72
+
73
+ config = _config(app_name)
74
+ _build(
75
+ source_dir=config.easy_sm_module_dir,
76
+ requirements_dir=config.requirements_dir,
77
+ docker_tag=obj['docker_tag'],
78
+ image_name=config.image_name,
79
+ python_version=config.python_version)
80
+
81
+ print("Docker image built successfully!")
@@ -0,0 +1,356 @@
1
+ import os
2
+ import sys
3
+ import click
4
+ from easy_sm.config.config import ConfigManager
5
+ from easy_sm.sagemaker import sagemaker
6
+
7
+
8
+ def _config(app_name):
9
+ config_file_path = os.path.join(f'{app_name}.json')
10
+ if not os.path.isfile(config_file_path):
11
+ raise ValueError("This is not a easy_sm directory: {}".format(os.getcwd()))
12
+ else:
13
+ return ConfigManager(config_file_path).get_config()
14
+
15
+
16
+ @click.group()
17
+ def cloud():
18
+ """
19
+ Commands for AWS operations: upload data, train and deploy
20
+ """
21
+ pass
22
+
23
+
24
+ @click.command(name='upload-data')
25
+ @click.option(u"-i", u"--input-dir", required=True, help="Path to data input directory")
26
+ @click.option(
27
+ u"-t", u"--target-dir",
28
+ required=True,
29
+ help="s3 location to upload data",
30
+ type=click.Path()
31
+ )
32
+ @click.option(
33
+ u"-r",
34
+ u"--iam-role-arn",
35
+ required=True,
36
+ help="The AWS role to use for the upload command"
37
+ )
38
+ @click.option(
39
+ u"-a",
40
+ u"--app-name",
41
+ required=True,
42
+ help="The app name whose json file will be referenced for setting up command"
43
+ )
44
+ def upload_data(input_dir, target_dir, iam_role_arn, app_name):
45
+ """
46
+ Command to upload data to S3
47
+ """
48
+ print("Started uploading data to S3...\n")
49
+ config = _config(app_name)
50
+ sage_maker_client = sagemaker.SageMakerClient(config.aws_profile, config.aws_region, iam_role_arn)
51
+ target_path = sage_maker_client.upload_data(input_dir, target_dir)
52
+ print("Data uploaded to {} successfully".format(target_path))
53
+
54
+
55
+ @click.command(name='train')
56
+ @click.option(
57
+ u"-i", u"--input-s3-dir",
58
+ required=True,
59
+ help="s3 location to input data",
60
+ type=click.Path()
61
+ )
62
+ @click.option(
63
+ u"-o", u"--output-s3-dir",
64
+ required=True,
65
+ help="s3 location to save output (models, etc)",
66
+ type=click.Path()
67
+ )
68
+ @click.option(u"-e", u"--ec2-type", required=True, help="ec2 instance type")
69
+ @click.option(
70
+ u"-r",
71
+ u"--iam-role-arn",
72
+ required=True,
73
+ help="The AWS role to use for the train command"
74
+ )
75
+ @click.option(
76
+ u"-n",
77
+ u"--base-job-name",
78
+ required=True,
79
+ help="Prefix for the SageMaker training job."
80
+ )
81
+ @click.option(
82
+ u"-a",
83
+ u"--app-name",
84
+ required=True,
85
+ help="The app name whose json file will be referenced for setting up command"
86
+ )
87
+ @click.pass_obj
88
+ def train(
89
+ obj,
90
+ input_s3_dir,
91
+ output_s3_dir,
92
+ ec2_type,
93
+ iam_role_arn,
94
+ base_job_name,
95
+ app_name
96
+ ):
97
+ """
98
+ Command to train ML model(s) on SageMaker
99
+ """
100
+
101
+ print("Started training on SageMaker...\n")
102
+ config = _config(app_name)
103
+ sage_maker_client = sagemaker.SageMakerClient(config.aws_profile, config.aws_region, iam_role_arn)
104
+
105
+ image_name = config.image_name+':'+obj['docker_tag']
106
+
107
+ s3_model_location = sage_maker_client.train(
108
+ image_name=image_name,
109
+ input_s3_data_location=input_s3_dir,
110
+ train_instance_type=ec2_type,
111
+ output_path=output_s3_dir,
112
+ base_job_name=base_job_name
113
+ )
114
+
115
+ print("Training on SageMaker succeeded")
116
+ print("Model S3 location: {}".format(s3_model_location))
117
+ return s3_model_location # To pipe into other commands
118
+
119
+
120
+ @click.command(name='deploy-serverless')
121
+ @click.option(
122
+ u"-m", u"--s3-model-location",
123
+ required=True,
124
+ help="s3 location to model tar.gz",
125
+ type=click.Path()
126
+ )
127
+ @click.option(u"-s",
128
+ u"--memory-size-in-mb",
129
+ required=True,
130
+ type=click.INT,
131
+ help="memory size in MB for serverless endpoint")
132
+ @click.option(
133
+ u"-r",
134
+ u"--iam-role-arn",
135
+ required=True,
136
+ help="The AWS role to use for the deploy command"
137
+ )
138
+ @click.option(
139
+ u"-n",
140
+ u"--endpoint-name",
141
+ required=True,
142
+ default=None,
143
+ help="Name for the SageMaker endpoint"
144
+ )
145
+ @click.option(
146
+ u"-a",
147
+ u"--app-name",
148
+ required=True,
149
+ help="The app name whose json file will be referenced for setting up command"
150
+ )
151
+ @click.pass_obj
152
+ def deploy_serverless(
153
+ obj,
154
+ s3_model_location,
155
+ memory_size_in_mb,
156
+ iam_role_arn,
157
+ endpoint_name,
158
+ app_name
159
+ ):
160
+ """
161
+ Command to deploy ML model(s) on SageMaker
162
+ """
163
+
164
+ print("Started deployment on SageMaker ...\n")
165
+ config = _config(app_name)
166
+ image_name = config.image_name+':'+obj['docker_tag']
167
+
168
+ sage_maker_client = sagemaker.SageMakerClient(config.aws_profile, config.aws_region, iam_role_arn)
169
+ endpoint_name = sage_maker_client.deploy_serverless(
170
+ image_name=image_name,
171
+ s3_model_location=s3_model_location,
172
+ memory_size_in_mb=memory_size_in_mb,
173
+ endpoint_name=endpoint_name
174
+ )
175
+
176
+ print("Endpoint name: {}".format(endpoint_name))
177
+
178
+
179
+ @click.command(name='batch-transform')
180
+ @click.option(
181
+ u"-m", u"--s3-model-location",
182
+ required=True,
183
+ help="s3 location to model tar.gz",
184
+ type=click.Path()
185
+ )
186
+ @click.option(
187
+ u"-i", u"--s3-input-location",
188
+ required=True,
189
+ help="s3 input data location",
190
+ type=click.Path()
191
+ )
192
+ @click.option(
193
+ u"-o", u"--s3-output-location",
194
+ required=True,
195
+ help="s3 location to save predictions",
196
+ type=click.Path()
197
+ )
198
+ @click.option(u"--num-instances", required=True, type=int, help="Number of ec2 instances")
199
+ @click.option(u"--ec2-type", required=True, help="ec2 instance type")
200
+ @click.option(
201
+ u"-r",
202
+ u"--iam-role-arn",
203
+ required=True,
204
+ help="The AWS role to use for batch transform command"
205
+ )
206
+ @click.option(
207
+ u"-w",
208
+ u"--wait",
209
+ default=False,
210
+ is_flag=True,
211
+ help="Wait until Batch Transform is finished. "
212
+ "Default: don't wait"
213
+ )
214
+ @click.option(
215
+ u"-n",
216
+ u"--job-name",
217
+ required=False,
218
+ default=None,
219
+ help="Name for the SageMaker batch transform job."
220
+ )
221
+ @click.option(
222
+ u"-a",
223
+ u"--app-name",
224
+ required=True,
225
+ help="The app name whose json file will be referenced for setting up command"
226
+ )
227
+ @click.pass_obj
228
+ def batch_transform(
229
+ obj,
230
+ s3_model_location,
231
+ s3_input_location,
232
+ s3_output_location,
233
+ num_instances,
234
+ ec2_type,
235
+ iam_role_arn,
236
+ wait,
237
+ job_name,
238
+ app_name
239
+ ):
240
+ """
241
+ Command to execute a batch transform job given a trained ML model on SageMaker
242
+ """
243
+ print("Started configuration of batch transform on SageMaker ...\n")
244
+
245
+ config = _config(app_name)
246
+ image_name = config.image_name+':'+obj['docker_tag']
247
+
248
+ sage_maker_client = sagemaker.SageMakerClient(config.aws_profile, config.aws_region, iam_role_arn)
249
+ status = sage_maker_client.batch_transform(
250
+ image_name=image_name,
251
+ s3_model_location=s3_model_location,
252
+ s3_input_location=s3_input_location,
253
+ s3_output_location=s3_output_location,
254
+ transform_instance_count=num_instances,
255
+ transform_instance_type=ec2_type,
256
+ wait=wait,
257
+ job_name=job_name
258
+ )
259
+
260
+ if wait:
261
+ print("Batch transform on SageMaker finished with status: {}".format(status))
262
+ if status == "Failed":
263
+ sys.exit(1)
264
+ else:
265
+ print("Started batch transform on SageMaker successfully")
266
+
267
+
268
+ @click.command(name='delete-endpoint')
269
+ @click.option(
270
+ u"-n",
271
+ u"--endpoint-name",
272
+ required=True,
273
+ default=None,
274
+ help="Name of the SageMaker endpoint"
275
+ )
276
+ @click.option(
277
+ u"-r",
278
+ u"--iam-role-arn",
279
+ required=True,
280
+ help="The AWS role to use for delete command"
281
+ )
282
+ @click.option(
283
+ u"-a",
284
+ u"--app-name",
285
+ required=True,
286
+ help="The app name whose json file will be referenced for setting up command"
287
+ )
288
+ def delete_endpoint(endpoint_name, iam_role_arn, app_name):
289
+ config = _config(app_name)
290
+ sage_maker_client = sagemaker.SageMakerClient(config.aws_profile, config.aws_region, iam_role_arn)
291
+ sage_maker_client.shutdown_endpoint(endpoint_name)
292
+ print(f"Endpoint {endpoint_name} has been deleted")
293
+
294
+
295
+ @click.command(name='process')
296
+ @click.option(u"-e", u"--ec2-type", required=True, help="ec2 instance type")
297
+ @click.option(
298
+ u"-r",
299
+ u"--iam-role-arn",
300
+ required=True,
301
+ help="The AWS role to use"
302
+ )
303
+ @click.option(
304
+ u"-n",
305
+ u"--base-job-name",
306
+ required=True,
307
+ help="Prefix for the SageMaker processing job."
308
+ "If not specified, default job name is generated, based on the docker image name and current timestamp."
309
+ )
310
+ @click.option(
311
+ u"-f",
312
+ u"--file",
313
+ required=True,
314
+ help="The name (not path) of python file to run as processing job"
315
+ )
316
+ @click.option(
317
+ u"-a",
318
+ u"--app-name",
319
+ required=True,
320
+ help="The app name whose json file will be referenced for setting up command"
321
+ )
322
+ @click.pass_obj
323
+ def process(
324
+ obj,
325
+ ec2_type,
326
+ iam_role_arn,
327
+ base_job_name,
328
+ file,
329
+ app_name
330
+ ):
331
+ """
332
+ Command to run python file as processing job on Sagemaker
333
+ """
334
+
335
+ print("Started processing job on SageMaker...\n")
336
+ config = _config(app_name)
337
+ sage_maker_client = sagemaker.SageMakerClient(config.aws_profile, config.aws_region, iam_role_arn)
338
+
339
+ image_name = config.image_name+':'+obj['docker_tag']
340
+
341
+ s3_model_location = sage_maker_client.process(
342
+ image_name=image_name,
343
+ processing_instance_type=ec2_type,
344
+ file=file,
345
+ base_job_name=base_job_name
346
+ )
347
+
348
+ print("Processing job on SageMaker succeeded")
349
+
350
+
351
+ cloud.add_command(upload_data)
352
+ cloud.add_command(train)
353
+ cloud.add_command(deploy_serverless)
354
+ cloud.add_command(batch_transform)
355
+ cloud.add_command(delete_endpoint)
356
+ cloud.add_command(process)
@@ -0,0 +1,169 @@
1
+ import os
2
+ import sys
3
+
4
+ import boto3
5
+ import click
6
+ from click import BadParameter
7
+ import os
8
+ from pathlib import Path
9
+ from easy_sm.config.config import ConfigManager
10
+ from distutils.dir_util import copy_tree
11
+
12
+ _FILE_DIR_PATH = os.path.dirname(os.path.realpath(__file__))
13
+
14
+
15
+ def _template_creation(app_name, aws_profile, aws_region, python_version, output_dir, requirements_dir):
16
+ easy_sm_module_name = 'easy_sm_base'
17
+
18
+ easy_sm_exists = os.path.exists(os.path.join(output_dir, easy_sm_module_name))
19
+ if easy_sm_exists:
20
+ raise ValueError(
21
+ "There is a easy_sm directory/module already. "
22
+ "Please, rename it in order to use easy_sm."
23
+ )
24
+
25
+ Path(output_dir).mkdir(exist_ok=True)
26
+ Path(os.path.join(output_dir, '__init__.py')).touch()
27
+
28
+ # Set 'easy_sm module' directory up
29
+ copy_tree(os.path.join(_FILE_DIR_PATH, '../template'), output_dir)
30
+
31
+ # Set configuration file up
32
+ config_manager = ConfigManager(os.path.join(f"{app_name}.json"))
33
+ config = config_manager.get_config()
34
+
35
+ config.image_name = app_name
36
+ config.aws_region = aws_region
37
+ config.aws_profile = aws_profile
38
+ config.easy_sm_module_dir = output_dir
39
+ config.python_version = python_version
40
+ config.requirements_dir = requirements_dir
41
+ config_manager.set_config(config)
42
+
43
+
44
+ def _get_local_aws_profiles():
45
+ return boto3.Session().available_profiles
46
+
47
+
48
+ def ask_for_app_name():
49
+ return click.prompt(
50
+ text="Type in a name for your SageMaker app (Only alphanumeric characters and - are allowed))",
51
+ type=str
52
+ )
53
+
54
+
55
+ def ask_if_existing_project_exists():
56
+ return click.confirm(text="Are you starting a new project?")
57
+
58
+
59
+ def ask_for_root_dir():
60
+ return click.prompt(text="Type in the directory where your code lives. Example: src", type=str).strip('/')
61
+
62
+
63
+ def ask_for_python_version():
64
+ print("Select Python interpreter:")
65
+ print('{}'.format('\n'.join(['1 - Python39', '2 - Python310', '3 - Python311'])))
66
+
67
+ def _validate_python_option(input_value):
68
+ if int(input_value) not in {1, 2, 3}:
69
+ raise BadParameter(
70
+ message="invalid choice: {}. (choose from 1, 2, 3)".format(str(input_value))
71
+ )
72
+
73
+ return int(input_value)
74
+
75
+ chosen_python_index = click.prompt(
76
+ text="Choose from 1, 2, 3",
77
+ default=1,
78
+ value_proc=lambda x: _validate_python_option(x)
79
+ )
80
+
81
+ _index_to_version = {
82
+ 1: '3.9',
83
+ 2: '3.10',
84
+ 3: '3.11'
85
+ }
86
+
87
+ return _index_to_version[chosen_python_index]
88
+
89
+
90
+ def ask_for_aws_details():
91
+ available_profiles = _get_local_aws_profiles()
92
+
93
+ if len(available_profiles) == 0:
94
+ print("aws cli is not configured!")
95
+ return
96
+
97
+ valid_positions = list(range(1, len(available_profiles) + 1))
98
+ print("Select AWS profile:")
99
+ print('{}'.format(
100
+ '\n'.join(
101
+ [
102
+ '{} - {}'.format(pos, profile)
103
+ for pos, profile in zip(valid_positions, available_profiles)
104
+ ]
105
+ )
106
+ )
107
+ )
108
+
109
+ def _validate_profile_position(input_pos):
110
+ if int(input_pos) not in valid_positions:
111
+ raise BadParameter(
112
+ message="invalid choice: {}. (choose from {})".format(
113
+ input_pos,
114
+ ', '.join([str(pos) for pos in valid_positions])
115
+ )
116
+ )
117
+ return int(input_pos)
118
+
119
+ chosen_profile_index = click.prompt(
120
+ text="Choose from {}".format(', '.join([str(pos) for pos in valid_positions])),
121
+ default=1,
122
+ value_proc=lambda x: _validate_profile_position(x)
123
+ )
124
+ chosen_profile_index -= 1
125
+
126
+ chosen_profile = available_profiles[chosen_profile_index]
127
+
128
+ chosen_region = click.prompt(
129
+ text="Type in your preferred AWS region name",
130
+ default='eu-west-1',
131
+ type=str
132
+ )
133
+
134
+ return chosen_profile, chosen_region
135
+
136
+
137
+ def ask_for_requirements_dir():
138
+ return click.prompt(text="Type in the path to requirements.txt. Example: requirements.txt", type=str).strip('/')
139
+
140
+
141
+ @click.command()
142
+ def init():
143
+ """
144
+ Command to initialize SageMaker template
145
+ """
146
+
147
+ easy_sm_app_name = ask_for_app_name()
148
+
149
+ is_new_project = ask_if_existing_project_exists()
150
+
151
+ root_dir = None
152
+ if not is_new_project:
153
+ root_dir = ask_for_root_dir()
154
+
155
+ python_version = ask_for_python_version()
156
+
157
+ aws_profile, aws_region = ask_for_aws_details()
158
+
159
+ requirements_dir = ask_for_requirements_dir()
160
+ _template_creation(
161
+ app_name=easy_sm_app_name,
162
+ aws_profile=aws_profile,
163
+ aws_region=aws_region,
164
+ python_version=python_version,
165
+ output_dir=root_dir if root_dir else easy_sm_app_name,
166
+ requirements_dir=requirements_dir
167
+ )
168
+
169
+ print("\neasy_sm module is created! ヽ(´▽`)/")