sagemaker-core 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 sagemaker-core might be problematic. Click here for more details.

@@ -0,0 +1,2122 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+ """Generates the resource classes for the service model."""
14
+ from collections import OrderedDict
15
+ import logging
16
+ from functools import lru_cache
17
+
18
+ import os
19
+ import json
20
+ from sagemaker_core.code_injection.codec import pascal_to_snake
21
+ from sagemaker_core.generated.config_schema import SAGEMAKER_PYTHON_SDK_CONFIG_SCHEMA
22
+ from sagemaker_core.generated.exceptions import IntelligentDefaultsError
23
+ from sagemaker_core.tools.constants import (
24
+ BASIC_RETURN_TYPES,
25
+ GENERATED_CLASSES_LOCATION,
26
+ RESOURCES_CODEGEN_FILE_NAME,
27
+ LICENCES_STRING,
28
+ TERMINAL_STATES,
29
+ BASIC_IMPORTS_STRING,
30
+ LOGGER_STRING,
31
+ CONFIG_SCHEMA_FILE_NAME,
32
+ PYTHON_TYPES_TO_BASIC_JSON_TYPES,
33
+ CONFIGURABLE_ATTRIBUTE_SUBSTRINGS,
34
+ )
35
+ from sagemaker_core.tools.method import Method, MethodType
36
+ from sagemaker_core.util.util import (
37
+ add_indent,
38
+ convert_to_snake_case,
39
+ snake_to_pascal,
40
+ remove_html_tags,
41
+ )
42
+ from sagemaker_core.tools.resources_extractor import ResourcesExtractor
43
+ from sagemaker_core.tools.shapes_extractor import ShapesExtractor
44
+ from sagemaker_core.tools.templates import (
45
+ CALL_OPERATION_API_NO_ARG_TEMPLATE,
46
+ CALL_OPERATION_API_TEMPLATE,
47
+ CREATE_METHOD_TEMPLATE,
48
+ DELETE_FAILED_STATUS_CHECK,
49
+ DELETED_STATUS_CHECK,
50
+ DESERIALIZE_INPUT_AND_RESPONSE_TO_CLS_TEMPLATE,
51
+ DESERIALIZE_RESPONSE_TEMPLATE,
52
+ DESERIALIZE_RESPONSE_TO_BASIC_TYPE_TEMPLATE,
53
+ GENERIC_METHOD_TEMPLATE,
54
+ GET_METHOD_TEMPLATE,
55
+ INITIALIZE_CLIENT_TEMPLATE,
56
+ REFRESH_METHOD_TEMPLATE,
57
+ RESOURCE_BASE_CLASS_TEMPLATE,
58
+ RETURN_ITERATOR_TEMPLATE,
59
+ SERIALIZE_INPUT_TEMPLATE,
60
+ SERIALIZE_LIST_INPUT_TEMPLATE,
61
+ STOP_METHOD_TEMPLATE,
62
+ DELETE_METHOD_TEMPLATE,
63
+ WAIT_FOR_DELETE_METHOD_TEMPLATE,
64
+ WAIT_METHOD_TEMPLATE,
65
+ WAIT_FOR_STATUS_METHOD_TEMPLATE,
66
+ UPDATE_METHOD_TEMPLATE,
67
+ POPULATE_DEFAULTS_DECORATOR_TEMPLATE,
68
+ CREATE_METHOD_TEMPLATE_WITHOUT_DEFAULTS,
69
+ INVOKE_METHOD_TEMPLATE,
70
+ INVOKE_ASYNC_METHOD_TEMPLATE,
71
+ INVOKE_WITH_RESPONSE_STREAM_METHOD_TEMPLATE,
72
+ IMPORT_METHOD_TEMPLATE,
73
+ FAILED_STATUS_ERROR_TEMPLATE,
74
+ GET_NAME_METHOD_TEMPLATE,
75
+ GET_ALL_METHOD_NO_ARGS_TEMPLATE,
76
+ GET_ALL_METHOD_WITH_ARGS_TEMPLATE,
77
+ UPDATE_METHOD_TEMPLATE_WITHOUT_DECORATOR,
78
+ RESOURCE_METHOD_EXCEPTION_DOCSTRING,
79
+ )
80
+ from sagemaker_core.tools.data_extractor import (
81
+ load_combined_shapes_data,
82
+ load_combined_operations_data,
83
+ )
84
+
85
+ logging.basicConfig(level=logging.INFO)
86
+ log = logging.getLogger(__name__)
87
+
88
+ TYPE = "type"
89
+ OBJECT = "object"
90
+ PROPERTIES = "properties"
91
+ SAGEMAKER = "SageMaker"
92
+ PYTHON_SDK = "PythonSDK"
93
+ SCHEMA_VERSION = "SchemaVersion"
94
+ RESOURCES = "Resources"
95
+ REQUIRED = "required"
96
+ GLOBAL_DEFAULTS = "GlobalDefaults"
97
+
98
+
99
+ class ResourcesCodeGen:
100
+ """
101
+ A class for generating resources based on a service JSON file.
102
+
103
+ Args:
104
+ service_json (dict): The Botocore service.json containing the shape definitions.
105
+
106
+ Attributes:
107
+ service_json (dict): The Botocore service.json containing the shape definitions.
108
+ version (str): The API version of the service.
109
+ protocol (str): The protocol used by the service.
110
+ service (str): The full name of the service.
111
+ service_id (str): The ID of the service.
112
+ uid (str): The unique identifier of the service.
113
+ operations (dict): The operations supported by the service.
114
+ shapes (dict): The shapes used by the service.
115
+ resources_extractor (ResourcesExtractor): An instance of the ResourcesExtractor class.
116
+ resources_plan (DataFrame): The resource plan in dataframe format.
117
+ shapes_extractor (ShapesExtractor): An instance of the ShapesExtractor class.
118
+
119
+ Raises:
120
+ Exception: If the service ID is not supported or the protocol is not supported.
121
+
122
+ """
123
+
124
+ def __init__(self, service_json: dict):
125
+ # Initialize the service_json dict
126
+ self.service_json = service_json
127
+
128
+ # Extract the metadata
129
+ metadata = self.service_json["metadata"]
130
+ self.version = metadata["apiVersion"]
131
+ self.protocol = metadata["protocol"]
132
+ self.service = metadata["serviceFullName"]
133
+ self.service_id = metadata["serviceId"]
134
+ self.uid = metadata["uid"]
135
+
136
+ # Check if the service ID and protocol are supported
137
+ if self.service_id != "SageMaker":
138
+ raise Exception(f"ServiceId {self.service_id} not supported in this resource generator")
139
+ if self.protocol != "json":
140
+ raise Exception(f"Protocol {self.protocol} not supported in this resource generator")
141
+
142
+ # Extract the operations and shapes
143
+ self.operations = load_combined_operations_data()
144
+ self.shapes = load_combined_shapes_data()
145
+
146
+ # Initialize the resources and shapes extractors
147
+ self.resources_extractor = ResourcesExtractor()
148
+ self.shapes_extractor = ShapesExtractor()
149
+
150
+ # Extract the resources plan and shapes DAG
151
+ self.resources_plan = self.resources_extractor.get_resource_plan()
152
+ self.resource_methods = self.resources_extractor.get_resource_methods()
153
+ self.shape_dag = self.shapes_extractor.get_shapes_dag()
154
+
155
+ # Create the Config Schema
156
+ self.generate_config_schema()
157
+ # Generate the resources
158
+ self.generate_resources()
159
+
160
+ def generate_license(self) -> str:
161
+ """
162
+ Generate the license for the generated resources file.
163
+
164
+ Returns:
165
+ str: The license.
166
+
167
+ """
168
+ return LICENCES_STRING
169
+
170
+ def generate_imports(self) -> str:
171
+ """
172
+ Generate the import statements for the generated resources file.
173
+
174
+ Returns:
175
+ str: The import statements.
176
+ """
177
+ # List of import statements
178
+ imports = [
179
+ BASIC_IMPORTS_STRING,
180
+ "import botocore",
181
+ "import datetime",
182
+ "import time",
183
+ "import functools",
184
+ "from pprint import pprint",
185
+ "from pydantic import validate_call",
186
+ "from typing import Dict, List, Literal, Optional, Union\n"
187
+ "from boto3.session import Session",
188
+ "from sagemaker_core.code_injection.codec import transform",
189
+ "from sagemaker_core.generated.utils import SageMakerClient, SageMakerRuntimeClient, ResourceIterator,"
190
+ " Unassigned, snake_to_pascal, pascal_to_snake, is_not_primitive, is_not_str_dict, is_snake_case, is_primitive_list",
191
+ "from sagemaker_core.generated.intelligent_defaults_helper import load_default_configs_for_resource_name, get_config_value",
192
+ "from sagemaker_core.generated.shapes import *",
193
+ "from sagemaker_core.generated.exceptions import *",
194
+ ]
195
+
196
+ formated_imports = "\n".join(imports)
197
+ formated_imports += "\n\n"
198
+
199
+ # Join the import statements with a newline character and return
200
+ return formated_imports
201
+
202
+ def generate_base_class(self) -> str:
203
+ """
204
+ Generate the base class for the resources.
205
+
206
+ Returns:
207
+ str: The base class.
208
+
209
+ """
210
+ return RESOURCE_BASE_CLASS_TEMPLATE
211
+
212
+ def generate_logging(self) -> str:
213
+ """
214
+ Generate the logging statements for the generated resources file.
215
+
216
+ Returns:
217
+ str: The logging statements.
218
+
219
+ """
220
+ return LOGGER_STRING
221
+
222
+ @staticmethod
223
+ def generate_defaults_decorator(
224
+ config_schema_for_resource: dict, resource_name: str, class_attributes: dict
225
+ ) -> str:
226
+ return POPULATE_DEFAULTS_DECORATOR_TEMPLATE.format(
227
+ config_schema_for_resource=add_indent(
228
+ json.dumps(config_schema_for_resource.get(PROPERTIES), indent=2), 4
229
+ ),
230
+ resource_name=resource_name,
231
+ configurable_attributes=CONFIGURABLE_ATTRIBUTE_SUBSTRINGS,
232
+ class_attributes=class_attributes,
233
+ )
234
+
235
+ def generate_resources(
236
+ self,
237
+ output_folder: str = GENERATED_CLASSES_LOCATION,
238
+ file_name: str = RESOURCES_CODEGEN_FILE_NAME,
239
+ ) -> None:
240
+ """
241
+ Generate the resources file.
242
+
243
+ Args:
244
+ output_folder (str, optional): The output folder path. Defaults to "GENERATED_CLASSES_LOCATION".
245
+ file_name (str, optional): The output file name. Defaults to "RESOURCES_CODEGEN_FILE_NAME".
246
+ """
247
+ # Check if the output folder exists, if not, create it
248
+ os.makedirs(output_folder, exist_ok=True)
249
+
250
+ # Create the full path for the output file
251
+ output_file = os.path.join(output_folder, file_name)
252
+
253
+ # Open the output file
254
+ with open(output_file, "w") as file:
255
+ # Generate and write the license to the file
256
+ file.write(self.generate_license())
257
+
258
+ # Generate and write the imports to the file
259
+ file.write(self.generate_imports())
260
+
261
+ # Generate and write the logging statements to the file
262
+ file.write(self.generate_logging())
263
+
264
+ # Generate and write the base class to the file
265
+ file.write(self.generate_base_class())
266
+
267
+ self.resource_names = [
268
+ row["resource_name"] for _, row in self.resources_plan.iterrows()
269
+ ]
270
+ # Iterate over the rows in the resources plan
271
+ for _, row in self.resources_plan.iterrows():
272
+ # Extract the necessary data from the row
273
+ resource_name = row["resource_name"]
274
+ class_methods = row["class_methods"]
275
+ object_methods = row["object_methods"]
276
+ additional_methods = row["additional_methods"]
277
+ raw_actions = row["raw_actions"]
278
+ resource_status_chain = row["resource_status_chain"]
279
+ resource_states = row["resource_states"]
280
+
281
+ # Generate the resource class
282
+ resource_class = self.generate_resource_class(
283
+ resource_name,
284
+ class_methods,
285
+ object_methods,
286
+ additional_methods,
287
+ raw_actions,
288
+ resource_status_chain,
289
+ resource_states,
290
+ )
291
+
292
+ # If the resource class was successfully generated, write it to the file
293
+ if resource_class:
294
+ file.write(f"{resource_class}\n\n")
295
+
296
+ def _evaluate_method(
297
+ self, resource_name: str, method_name: str, methods: list, **kwargs
298
+ ) -> str:
299
+ """Evaluate the specified method for a resource.
300
+
301
+ Args:
302
+ resource_name (str): The name of the resource.
303
+ method_name (str): The name of the method to evaluate.
304
+ methods (list): The list of methods for the resource.
305
+
306
+ Returns:
307
+ str: Formatted method if needed for a resource, else returns an empty string.
308
+ """
309
+ if method_name in methods:
310
+ return getattr(self, f"generate_{method_name}_method")(resource_name, **kwargs)
311
+ else:
312
+ # log.warning(f"Resource {resource_name} does not have a {method_name.upper()} method")
313
+ return ""
314
+
315
+ def generate_resource_class(
316
+ self,
317
+ resource_name: str,
318
+ class_methods: list,
319
+ object_methods: list,
320
+ additional_methods: list,
321
+ raw_actions: list,
322
+ resource_status_chain: list,
323
+ resource_states: list,
324
+ ) -> str:
325
+ """
326
+ Generate the resource class for a resource.
327
+
328
+ Args:
329
+ resource_name (str): The name of the resource.
330
+ class_methods (list): The class methods.
331
+ object_methods (list): The object methods.
332
+ additional_methods (list): The additional methods.
333
+ raw_actions (list): The raw actions.
334
+
335
+ Returns:
336
+ str: The formatted resource class.
337
+
338
+ """
339
+ # Initialize an empty string for the resource class
340
+ resource_class = ""
341
+
342
+ # _get_class_attributes will return value only if the resource has get or get_all method
343
+ if class_attribute_info := self._get_class_attributes(resource_name, class_methods):
344
+ class_attributes, class_attributes_string, attributes_and_documentation = (
345
+ class_attribute_info
346
+ )
347
+ # Start defining the class
348
+ resource_class = f"class {resource_name}(Base):\n"
349
+
350
+ class_documentation_string = f"Class representing resource {resource_name}\n\n"
351
+ class_documentation_string += f"Attributes:\n"
352
+ class_documentation_string += self._get_shape_attr_documentation_string(
353
+ attributes_and_documentation
354
+ )
355
+ resource_attributes = list(class_attributes.keys())
356
+
357
+ defaults_decorator_method = ""
358
+ # Check if 'create' is in the class methods
359
+ if "create" in class_methods or "update" in class_methods:
360
+ if config_schema_for_resource := self._get_config_schema_for_resources().get(
361
+ resource_name
362
+ ):
363
+ defaults_decorator_method = self.generate_defaults_decorator(
364
+ resource_name=resource_name,
365
+ class_attributes=class_attributes,
366
+ config_schema_for_resource=config_schema_for_resource,
367
+ )
368
+ needs_defaults_decorator = defaults_decorator_method != ""
369
+
370
+ # Add the class attributes and methods to the class definition
371
+ resource_class += add_indent(f'"""\n{class_documentation_string}\n"""\n', 4)
372
+
373
+ # Add the class attributes and methods to the class definition
374
+ resource_class += add_indent(class_attributes_string, 4)
375
+
376
+ resource_lower = convert_to_snake_case(resource_name)
377
+ get_name_method = self.generate_get_name_method(resource_lower=resource_lower)
378
+ resource_class += add_indent(get_name_method, 4)
379
+
380
+ if defaults_decorator_method:
381
+ resource_class += "\n"
382
+ resource_class += add_indent(defaults_decorator_method, 4)
383
+
384
+ if create_method := self._evaluate_method(
385
+ resource_name,
386
+ "create",
387
+ class_methods,
388
+ needs_defaults_decorator=needs_defaults_decorator,
389
+ ):
390
+ resource_class += add_indent(create_method, 4)
391
+
392
+ if get_method := self._evaluate_method(
393
+ resource_name,
394
+ "get",
395
+ class_methods,
396
+ ):
397
+ resource_class += add_indent(get_method, 4)
398
+
399
+ if refresh_method := self._evaluate_method(
400
+ resource_name, "refresh", object_methods, resource_attributes=resource_attributes
401
+ ):
402
+ resource_class += add_indent(refresh_method, 4)
403
+
404
+ if update_method := self._evaluate_method(
405
+ resource_name,
406
+ "update",
407
+ object_methods,
408
+ resource_attributes=resource_attributes,
409
+ needs_defaults_decorator=needs_defaults_decorator,
410
+ ):
411
+ resource_class += add_indent(update_method, 4)
412
+
413
+ if delete_method := self._evaluate_method(
414
+ resource_name, "delete", object_methods, resource_attributes=resource_attributes
415
+ ):
416
+ resource_class += add_indent(delete_method, 4)
417
+
418
+ if stop_method := self._evaluate_method(resource_name, "stop", object_methods):
419
+ resource_class += add_indent(stop_method, 4)
420
+
421
+ if wait_method := self._evaluate_method(resource_name, "wait", object_methods):
422
+ resource_class += add_indent(wait_method, 4)
423
+
424
+ if wait_for_status_method := self._evaluate_method(
425
+ resource_name, "wait_for_status", object_methods
426
+ ):
427
+ resource_class += add_indent(wait_for_status_method, 4)
428
+
429
+ if wait_for_delete_method := self._evaluate_method(
430
+ resource_name, "wait_for_delete", object_methods
431
+ ):
432
+ resource_class += add_indent(wait_for_delete_method, 4)
433
+
434
+ if invoke_method := self._evaluate_method(
435
+ resource_name,
436
+ "invoke",
437
+ object_methods,
438
+ resource_attributes=resource_attributes,
439
+ ):
440
+ resource_class += add_indent(invoke_method, 4)
441
+
442
+ if invoke_async_method := self._evaluate_method(
443
+ resource_name,
444
+ "invoke_async",
445
+ object_methods,
446
+ resource_attributes=resource_attributes,
447
+ ):
448
+ resource_class += add_indent(invoke_async_method, 4)
449
+
450
+ if invoke_with_response_stream_method := self._evaluate_method(
451
+ resource_name,
452
+ "invoke_with_response_stream",
453
+ object_methods,
454
+ resource_attributes=resource_attributes,
455
+ ):
456
+ resource_class += add_indent(invoke_with_response_stream_method, 4)
457
+
458
+ if import_method := self._evaluate_method(resource_name, "import", class_methods):
459
+ resource_class += add_indent(import_method, 4)
460
+
461
+ if list_method := self._evaluate_method(resource_name, "get_all", class_methods):
462
+ resource_class += add_indent(list_method, 4)
463
+
464
+ else:
465
+ # If there's no 'get' or 'list' or 'create' method, generate a class with no attributes
466
+ resource_attributes = []
467
+ resource_class = f"class {resource_name}(Base):\n"
468
+ class_documentation_string = f"Class representing resource {resource_name}\n"
469
+ resource_class += add_indent(f'"""\n{class_documentation_string}\n"""\n', 4)
470
+
471
+ if resource_name in self.resource_methods:
472
+ # TODO: use resource_methods for all methods
473
+ for method in self.resource_methods[resource_name].values():
474
+ formatted_method = self.generate_method(method, resource_attributes)
475
+ resource_class += add_indent(formatted_method, 4)
476
+
477
+ # Return the class definition
478
+ return resource_class
479
+
480
+ def _get_class_attributes(self, resource_name: str, class_methods: list) -> tuple:
481
+ """Get the class attributes for a resource.
482
+
483
+ Args:
484
+ resource_name (str): The name of the resource.
485
+ class_methods (list): The class methods of the resource. Now it can only get the class
486
+ attributes if the resource has get or get_all method.
487
+
488
+ Returns:
489
+ tuple:
490
+ class_attributes: The class attributes and the formatted class attributes string.
491
+ class_attributes_string: The code string of the class attributes
492
+ attributes_and_documentation: A dict of doc strings of the class attributes
493
+ """
494
+ if "get" in class_methods:
495
+ # Get the operation and shape for the 'get' method
496
+ get_operation = self.operations["Describe" + resource_name]
497
+ get_operation_shape = get_operation["output"]["shape"]
498
+
499
+ # Use 'get' operation input as the required class attributes.
500
+ # These are the mimumum identifing attributes for a resource object (ie, required for refresh())
501
+ get_operation_input_shape = get_operation["input"]["shape"]
502
+ required_attributes = self.shapes[get_operation_input_shape].get("required", [])
503
+
504
+ # Generate the class attributes based on the shape
505
+ class_attributes, class_attributes_string = (
506
+ self.shapes_extractor.generate_data_shape_members_and_string_body(
507
+ shape=get_operation_shape, required_override=tuple(required_attributes)
508
+ )
509
+ )
510
+ attributes_and_documentation = (
511
+ self.shapes_extractor.fetch_shape_members_and_doc_strings(get_operation_shape)
512
+ )
513
+ # Some resources are configured in the service.json inconsistently.
514
+ # These resources take in the main identifier in the create and get methods , but is not present in the describe response output
515
+ # Hence for consistent behaviour of functions such as refresh and delete, the identifiers are hardcoded
516
+ if resource_name == "ImageVersion":
517
+ class_attributes["image_name"] = "str"
518
+ class_attributes_string = "image_name: str\n" + class_attributes_string
519
+ if resource_name == "Workteam":
520
+ class_attributes["workteam_name"] = "str"
521
+ class_attributes_string = "workteam_name: str\n" + class_attributes_string
522
+ if resource_name == "Workforce":
523
+ class_attributes["workforce_name"] = "str"
524
+ class_attributes_string = "workforce_name: str\n" + class_attributes_string
525
+ if resource_name == "SubscribedWorkteam":
526
+ class_attributes["workteam_arn"] = "str"
527
+ class_attributes_string = "workteam_arn: str\n" + class_attributes_string
528
+
529
+ if resource_name == "HubContent":
530
+ class_attributes["hub_name"] = "Optional[str] = Unassigned()"
531
+ class_attributes_string = class_attributes_string.replace("hub_name: str", "")
532
+ class_attributes_string = (
533
+ class_attributes_string + "hub_name: Optional[str] = Unassigned()"
534
+ )
535
+
536
+ return class_attributes, class_attributes_string, attributes_and_documentation
537
+ elif "get_all" in class_methods:
538
+ # Get the operation and shape for the 'get_all' method
539
+ list_operation = self.operations["List" + resource_name + "s"]
540
+ list_operation_output_shape = list_operation["output"]["shape"]
541
+ list_operation_output_members = self.shapes[list_operation_output_shape]["members"]
542
+
543
+ # Use the object shape of 'get_all' operation output as the required class attributes.
544
+ filtered_list_operation_output_members = next(
545
+ {key: value}
546
+ for key, value in list_operation_output_members.items()
547
+ if key != "NextToken"
548
+ )
549
+
550
+ summaries_key = next(iter(filtered_list_operation_output_members))
551
+ summaries_shape_name = filtered_list_operation_output_members[summaries_key]["shape"]
552
+ summary_name = self.shapes[summaries_shape_name]["member"]["shape"]
553
+ required_attributes = self.shapes[summary_name].get("required", [])
554
+ # Generate the class attributes based on the shape
555
+ class_attributes, class_attributes_string = (
556
+ self.shapes_extractor.generate_data_shape_members_and_string_body(
557
+ shape=summary_name, required_override=tuple(required_attributes)
558
+ )
559
+ )
560
+ attributes_and_documentation = (
561
+ self.shapes_extractor.fetch_shape_members_and_doc_strings(summary_name)
562
+ )
563
+ return class_attributes, class_attributes_string, attributes_and_documentation
564
+ elif "create" in class_methods:
565
+ # Get the operation and shape for the 'create' method
566
+ create_operation = self.operations["Create" + resource_name]
567
+ create_operation_input_shape = create_operation["input"]["shape"]
568
+ create_operation_output_shape = create_operation["output"]["shape"]
569
+ # Generate the class attributes based on the input and output shape
570
+ class_attributes, class_attributes_string = self._get_resource_members_and_string_body(
571
+ resource_name, create_operation_input_shape, create_operation_output_shape
572
+ )
573
+ attributes_and_documentation = self._get_resouce_attributes_and_documentation(
574
+ create_operation_input_shape, create_operation_output_shape
575
+ )
576
+ return class_attributes, class_attributes_string, attributes_and_documentation
577
+ else:
578
+ return None
579
+
580
+ def _get_resource_members_and_string_body(self, resource_name: str, input_shape, output_shape):
581
+ input_members = self.shapes_extractor.generate_shape_members(input_shape)
582
+ output_members = self.shapes_extractor.generate_shape_members(output_shape)
583
+ resource_members = {**input_members, **output_members}
584
+ # bring the required members in front
585
+ ordered_members = {
586
+ attr: value
587
+ for attr, value in resource_members.items()
588
+ if not value.startswith("Optional")
589
+ }
590
+ ordered_members.update(resource_members)
591
+
592
+ resource_name_snake_case = pascal_to_snake(resource_name)
593
+ resource_names = [row["resource_name"] for _, row in self.resources_plan.iterrows()]
594
+ init_data_body = ""
595
+ for attr, value in ordered_members.items():
596
+ if (
597
+ resource_names
598
+ and attr.endswith("name")
599
+ and attr[: -len("_name")] != resource_name_snake_case
600
+ and attr != "name"
601
+ and snake_to_pascal(attr[: -len("_name")]) in resource_names
602
+ ):
603
+ if value.startswith("Optional"):
604
+ init_data_body += f"{attr}: Optional[Union[str, object]] = Unassigned()\n"
605
+ else:
606
+ init_data_body += f"{attr}: Union[str, object]\n"
607
+ elif attr == "lambda":
608
+ init_data_body += f"# {attr}: {value}\n"
609
+ else:
610
+ init_data_body += f"{attr}: {value}\n"
611
+ return ordered_members, init_data_body
612
+
613
+ def _get_resouce_attributes_and_documentation(self, input_shape, output_shape):
614
+ input_members = self.shapes[input_shape]["members"]
615
+ required_args = set(self.shapes[input_shape].get("required", []))
616
+ output_members = self.shapes[output_shape]["members"]
617
+ members = {**input_members, **output_members}
618
+ required_args.update(self.shapes[output_shape].get("required", []))
619
+ # bring the required members in front
620
+ ordered_members = {key: members[key] for key in members if key in required_args}
621
+ ordered_members.update(members)
622
+ shape_members_and_docstrings = {}
623
+ for member_name, member_attrs in ordered_members.items():
624
+ member_shape_documentation = member_attrs.get("documentation")
625
+ shape_members_and_docstrings[member_name] = member_shape_documentation
626
+ return shape_members_and_docstrings
627
+
628
+ def _get_shape_attr_documentation_string(
629
+ self, attributes_and_documentation, exclude_resource_attrs=None
630
+ ) -> str:
631
+ documentation_string = ""
632
+ for attribute, documentation in attributes_and_documentation.items():
633
+ attribute_snake = pascal_to_snake(attribute)
634
+ if exclude_resource_attrs and attribute_snake in exclude_resource_attrs:
635
+ # exclude resource attributes from documentation
636
+ continue
637
+ else:
638
+ if documentation == None:
639
+ documentation_string += f"{attribute_snake}: \n"
640
+ else:
641
+ documentation_string += f"{attribute_snake}: {documentation}\n"
642
+ documentation_string = add_indent(documentation_string)
643
+ return remove_html_tags(documentation_string)
644
+
645
+ def _generate_create_method_args(
646
+ self, operation_input_shape_name: str, resource_name: str
647
+ ) -> str:
648
+ """Generates the arguments for a method.
649
+ Args:
650
+ operation_input_shape_name (str): The name of the input shape for the operation.
651
+ Returns:
652
+ str: The generated arguments string.
653
+ """
654
+ typed_shape_members = self.shapes_extractor.generate_shape_members(
655
+ operation_input_shape_name
656
+ )
657
+ resource_name_in_snake_case = pascal_to_snake(resource_name)
658
+ method_args = ""
659
+ last_key = list(typed_shape_members.keys())[-1]
660
+ for attr, attr_type in typed_shape_members.items():
661
+ method_parameter_type = attr_type
662
+ if (
663
+ attr.endswith("name")
664
+ and attr[: -len("_name")] != resource_name_in_snake_case
665
+ and attr != "name"
666
+ and snake_to_pascal(attr[: -len("_name")]) in self.resource_names
667
+ ):
668
+ if attr_type.startswith("Optional"):
669
+ method_args += f"{attr}: Optional[Union[str, object]] = Unassigned(),"
670
+ else:
671
+ method_args += f"{attr}: Union[str, object],"
672
+ else:
673
+ method_args += f"{attr}: {method_parameter_type},"
674
+ if attr != last_key:
675
+ method_args += "\n"
676
+ method_args = add_indent(method_args)
677
+ return method_args
678
+
679
+ # TODO: use this method to replace _generate_operation_input_args
680
+ def _generate_operation_input_args_updated(
681
+ self,
682
+ resource_operation: dict,
683
+ is_class_method: bool,
684
+ resource_attributes: list = [],
685
+ exclude_list: list = [],
686
+ ) -> str:
687
+ """Generate the operation input arguments string.
688
+
689
+ Args:
690
+ resource_operation (dict): The resource operation dictionary.
691
+ is_class_method (bool): Indicates method is class method, else object method.
692
+
693
+ Returns:
694
+ str: The formatted operation input arguments string.
695
+ """
696
+ input_shape_name = resource_operation["input"]["shape"]
697
+ input_shape_members = list(self.shapes[input_shape_name]["members"].keys())
698
+
699
+ if is_class_method:
700
+ args = (
701
+ f"'{member}': {convert_to_snake_case(member)}"
702
+ for member in input_shape_members
703
+ if convert_to_snake_case(member) not in exclude_list
704
+ )
705
+ else:
706
+ args = []
707
+ for member in input_shape_members:
708
+ if convert_to_snake_case(member) not in exclude_list:
709
+ if convert_to_snake_case(member) in resource_attributes:
710
+ args.append(f"'{member}': self.{convert_to_snake_case(member)}")
711
+ else:
712
+ args.append(f"'{member}': {convert_to_snake_case(member)}")
713
+
714
+ operation_input_args = ",\n".join(args)
715
+ operation_input_args += ","
716
+ operation_input_args = add_indent(operation_input_args, 8)
717
+
718
+ return operation_input_args
719
+
720
+ def _generate_operation_input_args(
721
+ self, resource_operation: dict, is_class_method: bool, exclude_list: list = []
722
+ ) -> str:
723
+ """Generate the operation input arguments string.
724
+
725
+ Args:
726
+ resource_operation (dict): The resource operation dictionary.
727
+ is_class_method (bool): Indicates method is class method, else object method.
728
+
729
+ Returns:
730
+ str: The formatted operation input arguments string.
731
+ """
732
+ input_shape_name = resource_operation["input"]["shape"]
733
+ input_shape_members = list(self.shapes[input_shape_name]["members"].keys())
734
+
735
+ if is_class_method:
736
+ args = (
737
+ f"'{member}': {convert_to_snake_case(member)}"
738
+ for member in input_shape_members
739
+ if convert_to_snake_case(member) not in exclude_list
740
+ )
741
+ else:
742
+ args = (
743
+ f"'{member}': self.{convert_to_snake_case(member)}"
744
+ for member in input_shape_members
745
+ if convert_to_snake_case(member) not in exclude_list
746
+ )
747
+
748
+ operation_input_args = ",\n".join(args)
749
+ operation_input_args += ","
750
+ operation_input_args = add_indent(operation_input_args, 8)
751
+
752
+ return operation_input_args
753
+
754
+ def _generate_operation_input_necessary_args(
755
+ self, resource_operation: dict, resource_attributes: list
756
+ ) -> str:
757
+ """
758
+ Generate the operation input arguments string.
759
+ This will try to re-use args from the object attributes if present and it not presebt will use te ones provided in the parameter.
760
+ Args:
761
+ resource_operation (dict): The resource operation dictionary.
762
+ is_class_method (bool): Indicates method is class method, else object method.
763
+
764
+ Returns:
765
+ str: The formatted operation input arguments string.
766
+ """
767
+ input_shape_name = resource_operation["input"]["shape"]
768
+ input_shape_members = list(self.shapes[input_shape_name]["members"].keys())
769
+
770
+ args = list()
771
+ for member in input_shape_members:
772
+ if convert_to_snake_case(member) in resource_attributes:
773
+ args.append(f"'{member}': self.{convert_to_snake_case(member)}")
774
+ else:
775
+ args.append(f"'{member}': {convert_to_snake_case(member)}")
776
+
777
+ operation_input_args = ",\n".join(args)
778
+ operation_input_args += ","
779
+ operation_input_args = add_indent(operation_input_args, 8)
780
+
781
+ return operation_input_args
782
+
783
+ def _generate_method_args(
784
+ self, operation_input_shape_name: str, exclude_list: list = []
785
+ ) -> str:
786
+ """Generates the arguments for a method.
787
+ This will exclude attributes in the exclude_list from the arguments. For example, This is used for update() method
788
+ which does not require the resource identifier attributes to be passed as arguments.
789
+
790
+ Args:
791
+ operation_input_shape_name (str): The name of the input shape for the operation.
792
+ exclude_list (list): The list of attributes to exclude from the arguments.
793
+
794
+ Returns:
795
+ str: The generated arguments string.
796
+ """
797
+ typed_shape_members = self.shapes_extractor.generate_shape_members(
798
+ operation_input_shape_name
799
+ )
800
+
801
+ args = (
802
+ f"{attr}: {attr_type}"
803
+ for attr, attr_type in typed_shape_members.items()
804
+ if attr not in exclude_list
805
+ )
806
+ method_args = ",\n".join(args)
807
+ if not method_args:
808
+ return ""
809
+ method_args += ","
810
+ method_args = add_indent(method_args)
811
+ return method_args
812
+
813
+ def _generate_get_args(self, resource_name: str, operation_input_shape_name: str) -> str:
814
+ """
815
+ Generates a resource identifier based on the required members for the Describe and Create operations.
816
+
817
+ Args:
818
+ resource_name (str): The name of the resource.
819
+ operation_input_shape_name (str): The name of the input shape for the operation.
820
+
821
+ Returns:
822
+ str: The generated resource identifier.
823
+ """
824
+ describe_operation = self.operations["Describe" + resource_name]
825
+ describe_operation_input_shape_name = describe_operation["input"]["shape"]
826
+
827
+ required_members = self.shapes_extractor.get_required_members(
828
+ describe_operation_input_shape_name
829
+ )
830
+
831
+ operation_required_members = self.shapes_extractor.get_required_members(
832
+ operation_input_shape_name
833
+ )
834
+
835
+ identifiers = []
836
+ for member in required_members:
837
+ if member not in operation_required_members:
838
+ identifiers.append(f"{member}=response['{snake_to_pascal(member)}']")
839
+ else:
840
+ identifiers.append(f"{member}={member}")
841
+
842
+ get_args = ", ".join(identifiers)
843
+ return get_args
844
+
845
+ def generate_create_method(self, resource_name: str, **kwargs) -> str:
846
+ """
847
+ Auto-generate the CREATE method for a resource.
848
+
849
+ Args:
850
+ resource_name (str): The resource name.
851
+
852
+ Returns:
853
+ str: The formatted Create Method template.
854
+
855
+ """
856
+ # Get the operation and shape for the 'create' method
857
+ operation_name = "Create" + resource_name
858
+ operation_metadata = self.operations[operation_name]
859
+ operation_input_shape_name = operation_metadata["input"]["shape"]
860
+
861
+ # Generate the arguments for the 'create' method
862
+ create_args = self._generate_create_method_args(operation_input_shape_name, resource_name)
863
+
864
+ operation_input_args = self._generate_operation_input_args(
865
+ operation_metadata, is_class_method=True
866
+ )
867
+
868
+ # Convert the resource name to snake case
869
+ resource_lower = convert_to_snake_case(resource_name)
870
+
871
+ # Convert the operation name to snake case
872
+ operation = convert_to_snake_case(operation_name)
873
+
874
+ docstring = self._generate_docstring(
875
+ title=f"Create a {resource_name} resource",
876
+ operation_name=operation_name,
877
+ resource_name=resource_name,
878
+ operation_input_shape_name=operation_input_shape_name,
879
+ include_session_region=True,
880
+ include_return_resource_docstring=True,
881
+ include_intelligent_defaults_errors=True,
882
+ )
883
+
884
+ if "Describe" + resource_name in self.operations:
885
+ # If the resource has Describe method, call Describe API and return its value
886
+ get_args = self._generate_get_args(resource_name, operation_input_shape_name)
887
+
888
+ # Format the method using the CREATE_METHOD_TEMPLATE
889
+ if kwargs["needs_defaults_decorator"]:
890
+ formatted_method = CREATE_METHOD_TEMPLATE.format(
891
+ docstring=docstring,
892
+ resource_name=resource_name,
893
+ create_args=create_args,
894
+ resource_lower=resource_lower,
895
+ service_name="sagemaker", # TODO: change service name based on the service - runtime, sagemaker, etc.
896
+ operation_input_args=operation_input_args,
897
+ operation=operation,
898
+ get_args=get_args,
899
+ )
900
+ else:
901
+ formatted_method = CREATE_METHOD_TEMPLATE_WITHOUT_DEFAULTS.format(
902
+ docstring=docstring,
903
+ resource_name=resource_name,
904
+ create_args=create_args,
905
+ resource_lower=resource_lower,
906
+ service_name="sagemaker", # TODO: change service name based on the service - runtime, sagemaker, etc.
907
+ operation_input_args=operation_input_args,
908
+ operation=operation,
909
+ get_args=get_args,
910
+ )
911
+ # Return the formatted method
912
+ return formatted_method
913
+ else:
914
+ # If the resource does not have Describe method, return a instance with
915
+ # the input and output of Create method
916
+ decorator = "@classmethod"
917
+ serialize_operation_input = SERIALIZE_INPUT_TEMPLATE.format(
918
+ operation_input_args=operation_input_args
919
+ )
920
+ initialize_client = INITIALIZE_CLIENT_TEMPLATE.format(service_name="sagemaker")
921
+ call_operation_api = CALL_OPERATION_API_TEMPLATE.format(
922
+ operation=convert_to_snake_case(operation_name)
923
+ )
924
+ operation_output_shape_name = operation_metadata["output"]["shape"]
925
+ deserialize_response = DESERIALIZE_INPUT_AND_RESPONSE_TO_CLS_TEMPLATE.format(
926
+ operation_output_shape=operation_output_shape_name
927
+ )
928
+ formatted_method = GENERIC_METHOD_TEMPLATE.format(
929
+ docstring=docstring,
930
+ decorator=decorator,
931
+ method_name="create",
932
+ method_args=add_indent("cls,\n", 4) + create_args,
933
+ return_type='Optional["resource_name"]',
934
+ serialize_operation_input=serialize_operation_input,
935
+ initialize_client=initialize_client,
936
+ call_operation_api=call_operation_api,
937
+ deserialize_response=deserialize_response,
938
+ )
939
+ # Return the formatted method
940
+ return formatted_method
941
+
942
+ @lru_cache
943
+ def _fetch_shape_errors_and_doc_strings(self, operation):
944
+ operation_dict = self.operations[operation]
945
+ errors = operation_dict.get("errors", [])
946
+ shape_errors_and_docstrings = {}
947
+ if errors:
948
+ for e in errors:
949
+ error_shape = e["shape"]
950
+ error_shape_dict = self.shapes[error_shape]
951
+ error_shape_documentation = error_shape_dict.get("documentation").strip()
952
+ shape_errors_and_docstrings[error_shape] = error_shape_documentation
953
+ sorted_keys = sorted(shape_errors_and_docstrings.keys())
954
+ return {key: shape_errors_and_docstrings[key] for key in sorted_keys}
955
+
956
+ def _exception_docstring(self, operation: str) -> str:
957
+ _docstring = RESOURCE_METHOD_EXCEPTION_DOCSTRING
958
+ for error, documentaion in self._fetch_shape_errors_and_doc_strings(operation).items():
959
+ _docstring += f"\n {error}: {remove_html_tags(documentaion).strip()}"
960
+ return _docstring
961
+
962
+ def _generate_docstring(
963
+ self,
964
+ title: str,
965
+ operation_name: str,
966
+ resource_name: str,
967
+ operation_input_shape_name: str = None,
968
+ include_session_region: bool = False,
969
+ include_return_resource_docstring: bool = False,
970
+ return_string: str = None,
971
+ include_intelligent_defaults_errors: bool = False,
972
+ exclude_resource_attrs: list = None,
973
+ ) -> str:
974
+ """
975
+ Generate the docstring for a method of a resource.
976
+
977
+ Args:
978
+ title (str): The title of the docstring.
979
+ operation_name (str): The name of the operation.
980
+ resource_name (str): The name of the resource.
981
+ operation_input_shape_name (str): The name of the operation input shape.
982
+ include_session_region (bool): Whether to include session and region documentation.
983
+ include_return_resource_docstring (bool): Whether to include resource-specific documentation.
984
+ return_string (str): The return string.
985
+ include_intelligent_defaults_errors (bool): Whether to include intelligent defaults errors.
986
+ exclude_resource_attrs (list): A list of attributes to exclude from the docstring.
987
+
988
+ Returns:
989
+ str: The generated docstring for the IMPORT method.
990
+ """
991
+ docstring = f"{title}\n"
992
+ if operation_input_shape_name:
993
+ _shape_attr_documentation_string = self._get_shape_attr_documentation_string(
994
+ self.shapes_extractor.fetch_shape_members_and_doc_strings(
995
+ operation_input_shape_name
996
+ ),
997
+ exclude_resource_attrs=exclude_resource_attrs,
998
+ )
999
+ if _shape_attr_documentation_string:
1000
+ docstring += f"\nParameters:\n"
1001
+ docstring += _shape_attr_documentation_string
1002
+
1003
+ if include_session_region:
1004
+ if not _shape_attr_documentation_string:
1005
+ docstring += f"\nParameters:\n"
1006
+ docstring += add_indent(f"session: Boto3 session.\nregion: Region name.\n")
1007
+
1008
+ if include_return_resource_docstring:
1009
+ docstring += f"\nReturns:\n" f" The {resource_name} resource.\n"
1010
+ elif return_string:
1011
+ docstring += "\n" + return_string
1012
+
1013
+ docstring += self._exception_docstring(operation_name)
1014
+
1015
+ if include_intelligent_defaults_errors:
1016
+ subclasses = set(IntelligentDefaultsError.__subclasses__())
1017
+ _id_exception_docstrings = [
1018
+ f"\n {subclass.__name__}: {subclass.__doc__}" for subclass in subclasses
1019
+ ]
1020
+ sorted_id_exception_docstrings = sorted(_id_exception_docstrings)
1021
+ docstring += "".join(sorted_id_exception_docstrings)
1022
+ docstring = add_indent(f'"""\n{docstring}\n"""\n', 4)
1023
+
1024
+ return docstring
1025
+
1026
+ def generate_import_method(self, resource_name: str) -> str:
1027
+ """
1028
+ Auto-generate the IMPORT method for a resource.
1029
+
1030
+ Args:
1031
+ resource_name (str): The resource name.
1032
+
1033
+ Returns:
1034
+ str: The formatted Import Method template.
1035
+
1036
+ """
1037
+ # Get the operation and shape for the 'import' method
1038
+ operation_name = "Import" + resource_name
1039
+ operation_metadata = self.operations[operation_name]
1040
+ operation_input_shape_name = operation_metadata["input"]["shape"]
1041
+
1042
+ # Generate the arguments for the 'import' method
1043
+ import_args = self._generate_method_args(operation_input_shape_name)
1044
+
1045
+ operation_input_args = self._generate_operation_input_args(
1046
+ operation_metadata, is_class_method=True
1047
+ )
1048
+
1049
+ # Convert the resource name to snake case
1050
+ resource_lower = convert_to_snake_case(resource_name)
1051
+
1052
+ # Convert the operation name to snake case
1053
+ operation = convert_to_snake_case(operation_name)
1054
+
1055
+ get_args = self._generate_get_args(resource_name, operation_input_shape_name)
1056
+
1057
+ docstring = self._generate_docstring(
1058
+ title=f"Import a {resource_name} resource",
1059
+ operation_name=operation_name,
1060
+ resource_name=resource_name,
1061
+ operation_input_shape_name=operation_input_shape_name,
1062
+ include_session_region=True,
1063
+ include_return_resource_docstring=True,
1064
+ )
1065
+
1066
+ # Format the method using the IMPORT_METHOD_TEMPLATE
1067
+ formatted_method = IMPORT_METHOD_TEMPLATE.format(
1068
+ docstring=docstring,
1069
+ resource_name=resource_name,
1070
+ import_args=import_args,
1071
+ resource_lower=resource_lower,
1072
+ service_name="sagemaker", # TODO: change service name based on the service - runtime, sagemaker, etc.
1073
+ operation_input_args=operation_input_args,
1074
+ operation=operation,
1075
+ get_args=get_args,
1076
+ )
1077
+
1078
+ # Return the formatted method
1079
+ return formatted_method
1080
+
1081
+ def generate_get_name_method(self, resource_lower: str) -> str:
1082
+ """
1083
+ Autogenerate the method that would return the identifier of the object
1084
+ Args:
1085
+ resource_name: Name of Resource
1086
+ Returns:
1087
+ str: Formatted Get Name Method
1088
+ """
1089
+ return GET_NAME_METHOD_TEMPLATE.format(resource_lower=resource_lower)
1090
+
1091
+ def generate_update_method(self, resource_name: str, **kwargs) -> str:
1092
+ """
1093
+ Auto-generate the UPDATE method for a resource.
1094
+
1095
+ Args:
1096
+ resource_name (str): The resource name.
1097
+
1098
+ Returns:
1099
+ str: The formatted Update Method template.
1100
+
1101
+ """
1102
+ # Get the operation and shape for the 'update' method
1103
+ operation_name = "Update" + resource_name
1104
+ operation_metadata = self.operations[operation_name]
1105
+ operation_input_shape_name = operation_metadata["input"]["shape"]
1106
+
1107
+ required_members = self.shapes[operation_input_shape_name]["required"]
1108
+
1109
+ # Exclude any required attributes that are already present as resource attributes and are also identifiers
1110
+ exclude_required_attributes = []
1111
+ for member in required_members:
1112
+ snake_member = convert_to_snake_case(member)
1113
+ if snake_member in kwargs["resource_attributes"] and any(
1114
+ id in snake_member for id in ["name", "arn", "id"]
1115
+ ):
1116
+ exclude_required_attributes.append(snake_member)
1117
+
1118
+ # Generate the arguments for the 'update' method
1119
+ update_args = self._generate_method_args(
1120
+ operation_input_shape_name, exclude_required_attributes
1121
+ )
1122
+
1123
+ operation_input_args = self._generate_operation_input_necessary_args(
1124
+ operation_metadata, exclude_required_attributes
1125
+ )
1126
+
1127
+ # Convert the resource name to snake case
1128
+ resource_lower = convert_to_snake_case(resource_name)
1129
+
1130
+ # Convert the operation name to snake case
1131
+ operation = convert_to_snake_case(operation_name)
1132
+
1133
+ docstring = self._generate_docstring(
1134
+ title=f"Update a {resource_name} resource",
1135
+ operation_name=operation_name,
1136
+ resource_name=resource_name,
1137
+ operation_input_shape_name=operation_input_shape_name,
1138
+ include_session_region=False,
1139
+ include_return_resource_docstring=True,
1140
+ exclude_resource_attrs=kwargs["resource_attributes"],
1141
+ )
1142
+
1143
+ # Format the method using the CREATE_METHOD_TEMPLATE
1144
+ if kwargs["needs_defaults_decorator"]:
1145
+ formatted_method = UPDATE_METHOD_TEMPLATE.format(
1146
+ docstring=docstring,
1147
+ service_name="sagemaker",
1148
+ resource_name=resource_name,
1149
+ resource_lower=resource_lower,
1150
+ update_args=update_args,
1151
+ operation_input_args=operation_input_args,
1152
+ operation=operation,
1153
+ )
1154
+ else:
1155
+ formatted_method = UPDATE_METHOD_TEMPLATE_WITHOUT_DECORATOR.format(
1156
+ docstring=docstring,
1157
+ service_name="sagemaker",
1158
+ resource_name=resource_name,
1159
+ resource_lower=resource_lower,
1160
+ update_args=update_args,
1161
+ operation_input_args=operation_input_args,
1162
+ operation=operation,
1163
+ )
1164
+
1165
+ # Return the formatted method
1166
+ return formatted_method
1167
+
1168
+ def generate_invoke_method(self, resource_name: str, **kwargs) -> str:
1169
+ """
1170
+ Auto-generate the INVOKE ASYNC method for a resource.
1171
+
1172
+ Args:
1173
+ resource_name (str): The resource name.
1174
+
1175
+ Returns:
1176
+ str: The formatted Update Method template.
1177
+
1178
+ """
1179
+ # Get the operation and shape for the 'create' method
1180
+ operation_name = "Invoke" + resource_name
1181
+ operation_metadata = self.operations[operation_name]
1182
+ operation_input_shape_name = operation_metadata["input"]["shape"]
1183
+
1184
+ # Generate the arguments for the 'create' method
1185
+ invoke_args = self._generate_method_args(
1186
+ operation_input_shape_name, kwargs["resource_attributes"]
1187
+ )
1188
+
1189
+ operation_input_args = self._generate_operation_input_necessary_args(
1190
+ operation_metadata, kwargs["resource_attributes"]
1191
+ )
1192
+
1193
+ # Convert the resource name to snake case
1194
+ resource_lower = convert_to_snake_case(resource_name)
1195
+
1196
+ # Convert the operation name to snake case
1197
+ operation = convert_to_snake_case(operation_name)
1198
+
1199
+ # generate docstring
1200
+ docstring = self._generate_docstring(
1201
+ title=f"Invoke a {resource_name} resource",
1202
+ operation_name=operation_name,
1203
+ resource_name=resource_name,
1204
+ operation_input_shape_name=operation_input_shape_name,
1205
+ include_session_region=False,
1206
+ include_return_resource_docstring=False,
1207
+ return_string=f"Returns:\n" f" The Invoke response.\n",
1208
+ exclude_resource_attrs=kwargs["resource_attributes"],
1209
+ )
1210
+ # Format the method using the CREATE_METHOD_TEMPLATE
1211
+ formatted_method = INVOKE_METHOD_TEMPLATE.format(
1212
+ docstring=docstring,
1213
+ service_name="sagemaker-runtime",
1214
+ invoke_args=invoke_args,
1215
+ resource_name=resource_name,
1216
+ resource_lower=resource_lower,
1217
+ operation_input_args=operation_input_args,
1218
+ operation=operation,
1219
+ )
1220
+
1221
+ # Return the formatted method
1222
+ return formatted_method
1223
+
1224
+ def generate_invoke_async_method(self, resource_name: str, **kwargs) -> str:
1225
+ """
1226
+ Auto-generate the INVOKE method for a resource.
1227
+
1228
+ Args:
1229
+ resource_name (str): The resource name.
1230
+
1231
+ Returns:
1232
+ str: The formatted Update Method template.
1233
+
1234
+ """
1235
+ # Get the operation and shape for the 'create' method
1236
+ operation_name = "Invoke" + resource_name + "Async"
1237
+ operation_metadata = self.operations[operation_name]
1238
+ operation_input_shape_name = operation_metadata["input"]["shape"]
1239
+
1240
+ # Generate the arguments for the 'create' method
1241
+ invoke_args = self._generate_method_args(
1242
+ operation_input_shape_name, kwargs["resource_attributes"]
1243
+ )
1244
+
1245
+ operation_input_args = self._generate_operation_input_necessary_args(
1246
+ operation_metadata, kwargs["resource_attributes"]
1247
+ )
1248
+
1249
+ # Convert the resource name to snake case
1250
+ resource_lower = convert_to_snake_case(resource_name)
1251
+
1252
+ # Convert the operation name to snake case
1253
+ operation = convert_to_snake_case(operation_name)
1254
+
1255
+ # generate docstring
1256
+ docstring = self._generate_docstring(
1257
+ title=f"Invoke Async a {resource_name} resource",
1258
+ operation_name=operation_name,
1259
+ resource_name=resource_name,
1260
+ operation_input_shape_name=operation_input_shape_name,
1261
+ include_session_region=False,
1262
+ include_return_resource_docstring=False,
1263
+ return_string=f"Returns:\n" f" The Invoke response.\n",
1264
+ exclude_resource_attrs=kwargs["resource_attributes"],
1265
+ )
1266
+ # Format the method using the CREATE_METHOD_TEMPLATE
1267
+ formatted_method = INVOKE_ASYNC_METHOD_TEMPLATE.format(
1268
+ docstring=docstring,
1269
+ service_name="sagemaker-runtime",
1270
+ create_args=invoke_args,
1271
+ resource_name=resource_name,
1272
+ resource_lower=resource_lower,
1273
+ operation_input_args=operation_input_args,
1274
+ operation=operation,
1275
+ )
1276
+
1277
+ # Return the formatted method
1278
+ return formatted_method
1279
+
1280
+ def generate_invoke_with_response_stream_method(self, resource_name: str, **kwargs) -> str:
1281
+ """
1282
+ Auto-generate the INVOKE with response stream method for a resource.
1283
+
1284
+ Args:
1285
+ resource_name (str): The resource name.
1286
+
1287
+ Returns:
1288
+ str: The formatted Update Method template.
1289
+
1290
+ """
1291
+ # Get the operation and shape for the 'create' method
1292
+ operation_name = "Invoke" + resource_name + "WithResponseStream"
1293
+ operation_metadata = self.operations[operation_name]
1294
+ operation_input_shape_name = operation_metadata["input"]["shape"]
1295
+
1296
+ # Generate the arguments for the 'create' method
1297
+ invoke_args = self._generate_method_args(
1298
+ operation_input_shape_name, kwargs["resource_attributes"]
1299
+ )
1300
+
1301
+ operation_input_args = self._generate_operation_input_necessary_args(
1302
+ operation_metadata, kwargs["resource_attributes"]
1303
+ )
1304
+
1305
+ # Convert the resource name to snake case
1306
+ resource_lower = convert_to_snake_case(resource_name)
1307
+
1308
+ # Convert the operation name to snake case
1309
+ operation = convert_to_snake_case(operation_name)
1310
+
1311
+ # generate docstring
1312
+ docstring = self._generate_docstring(
1313
+ title=f"Invoke with response stream a {resource_name} resource",
1314
+ operation_name=operation_name,
1315
+ resource_name=resource_name,
1316
+ operation_input_shape_name=operation_input_shape_name,
1317
+ include_session_region=False,
1318
+ include_return_resource_docstring=False,
1319
+ return_string=f"Returns:\n" f" The Invoke response.\n",
1320
+ exclude_resource_attrs=kwargs["resource_attributes"],
1321
+ )
1322
+ # Format the method using the CREATE_METHOD_TEMPLATE
1323
+ formatted_method = INVOKE_WITH_RESPONSE_STREAM_METHOD_TEMPLATE.format(
1324
+ docstring=docstring,
1325
+ service_name="sagemaker-runtime",
1326
+ create_args=invoke_args,
1327
+ resource_name=resource_name,
1328
+ resource_lower=resource_lower,
1329
+ operation_input_args=operation_input_args,
1330
+ operation=operation,
1331
+ )
1332
+
1333
+ # Return the formatted method
1334
+ return formatted_method
1335
+
1336
+ def generate_get_method(self, resource_name: str) -> str:
1337
+ """
1338
+ Auto-generate the GET method (describe API) for a resource.
1339
+
1340
+ Args:
1341
+ resource_name (str): The resource name.
1342
+
1343
+ Returns:
1344
+ str: The formatted Get Method template.
1345
+
1346
+ """
1347
+ operation_name = "Describe" + resource_name
1348
+ operation_metadata = self.operations[operation_name]
1349
+ resource_operation_input_shape_name = operation_metadata["input"]["shape"]
1350
+ resource_operation_output_shape_name = operation_metadata["output"]["shape"]
1351
+
1352
+ operation_input_args = self._generate_operation_input_args(
1353
+ operation_metadata, is_class_method=True
1354
+ )
1355
+
1356
+ # Generate the arguments for the 'update' method
1357
+ describe_args = self._generate_method_args(resource_operation_input_shape_name)
1358
+
1359
+ resource_lower = convert_to_snake_case(resource_name)
1360
+
1361
+ operation = convert_to_snake_case(operation_name)
1362
+
1363
+ # generate docstring
1364
+ docstring = self._generate_docstring(
1365
+ title=f"Get a {resource_name} resource",
1366
+ operation_name=operation_name,
1367
+ resource_name=resource_name,
1368
+ operation_input_shape_name=resource_operation_input_shape_name,
1369
+ include_session_region=True,
1370
+ include_return_resource_docstring=True,
1371
+ )
1372
+
1373
+ formatted_method = GET_METHOD_TEMPLATE.format(
1374
+ docstring=docstring,
1375
+ resource_name=resource_name,
1376
+ service_name="sagemaker", # TODO: change service name based on the service - runtime, sagemaker, etc.
1377
+ describe_args=describe_args,
1378
+ resource_lower=resource_lower,
1379
+ operation_input_args=operation_input_args,
1380
+ operation=operation,
1381
+ describe_operation_output_shape=resource_operation_output_shape_name,
1382
+ )
1383
+ return formatted_method
1384
+
1385
+ def generate_refresh_method(self, resource_name: str, **kwargs) -> str:
1386
+ """Auto-Generate 'refresh' object Method [describe API] for a resource.
1387
+
1388
+ Args:
1389
+ resource_name (str): The resource name.
1390
+
1391
+ Returns:
1392
+ str: The formatted refresh Method template.
1393
+ """
1394
+ operation_name = "Describe" + resource_name
1395
+ operation_metadata = self.operations[operation_name]
1396
+ resource_operation_input_shape_name = operation_metadata["input"]["shape"]
1397
+ resource_operation_output_shape_name = operation_metadata["output"]["shape"]
1398
+
1399
+ # Generate the arguments for the 'refresh' method
1400
+ refresh_args = self._generate_method_args(
1401
+ resource_operation_input_shape_name, kwargs["resource_attributes"]
1402
+ )
1403
+
1404
+ operation_input_args = self._generate_operation_input_necessary_args(
1405
+ operation_metadata, kwargs["resource_attributes"]
1406
+ )
1407
+
1408
+ operation = convert_to_snake_case(operation_name)
1409
+
1410
+ # generate docstring
1411
+ docstring = self._generate_docstring(
1412
+ title=f"Refresh a {resource_name} resource",
1413
+ operation_name=operation_name,
1414
+ resource_name=resource_name,
1415
+ include_session_region=False,
1416
+ include_return_resource_docstring=True,
1417
+ )
1418
+
1419
+ formatted_method = REFRESH_METHOD_TEMPLATE.format(
1420
+ docstring=docstring,
1421
+ resource_name=resource_name,
1422
+ operation_input_args=operation_input_args,
1423
+ refresh_args=refresh_args,
1424
+ operation=operation,
1425
+ describe_operation_output_shape=resource_operation_output_shape_name,
1426
+ )
1427
+ return formatted_method
1428
+
1429
+ def generate_delete_method(self, resource_name: str, **kwargs) -> str:
1430
+ """Auto-Generate 'delete' object Method [delete API] for a resource.
1431
+
1432
+ Args:
1433
+ resource_name (str): The resource name.
1434
+
1435
+ Returns:
1436
+ str: The formatted delete Method template.
1437
+ """
1438
+ operation_name = "Delete" + resource_name
1439
+ operation_metadata = self.operations[operation_name]
1440
+ resource_operation_input_shape_name = operation_metadata["input"]["shape"]
1441
+
1442
+ # Generate the arguments for the 'update' method
1443
+ delete_args = self._generate_method_args(
1444
+ resource_operation_input_shape_name, kwargs["resource_attributes"]
1445
+ )
1446
+ operation_input_args = self._generate_operation_input_necessary_args(
1447
+ operation_metadata, kwargs["resource_attributes"]
1448
+ )
1449
+
1450
+ operation = convert_to_snake_case(operation_name)
1451
+
1452
+ # generate docstring
1453
+ docstring = self._generate_docstring(
1454
+ title=f"Delete a {resource_name} resource",
1455
+ operation_name=operation_name,
1456
+ resource_name=resource_name,
1457
+ include_session_region=False,
1458
+ include_return_resource_docstring=False,
1459
+ )
1460
+
1461
+ formatted_method = DELETE_METHOD_TEMPLATE.format(
1462
+ docstring=docstring,
1463
+ resource_name=resource_name,
1464
+ delete_args=delete_args,
1465
+ operation_input_args=operation_input_args,
1466
+ operation=operation,
1467
+ )
1468
+ return formatted_method
1469
+
1470
+ def generate_stop_method(self, resource_name: str) -> str:
1471
+ """Auto-Generate 'stop' object Method [delete API] for a resource.
1472
+
1473
+ Args:
1474
+ resource_name (str): The resource name.
1475
+
1476
+ Returns:
1477
+ str: The formatted stop Method template.
1478
+ """
1479
+ operation_name = "Stop" + resource_name
1480
+ operation_metadata = self.operations[operation_name]
1481
+
1482
+ operation_input_args = self._generate_operation_input_args(
1483
+ operation_metadata, is_class_method=False
1484
+ )
1485
+
1486
+ operation = convert_to_snake_case(operation_name)
1487
+
1488
+ # generate docstring
1489
+ docstring = self._generate_docstring(
1490
+ title=f"Stop a {resource_name} resource",
1491
+ operation_name=operation_name,
1492
+ resource_name=resource_name,
1493
+ include_session_region=False,
1494
+ include_return_resource_docstring=False,
1495
+ )
1496
+
1497
+ formatted_method = STOP_METHOD_TEMPLATE.format(
1498
+ docstring=docstring,
1499
+ resource_name=resource_name,
1500
+ operation_input_args=operation_input_args,
1501
+ operation=operation,
1502
+ )
1503
+ return formatted_method
1504
+
1505
+ def generate_method(self, method: Method, resource_attributes: list):
1506
+ # TODO: Use special templates for some methods with different formats like list and wait
1507
+ if method.method_name.startswith("get_all"):
1508
+ return self.generate_additional_get_all_method(method, resource_attributes)
1509
+ operation_metadata = self.operations[method.operation_name]
1510
+ operation_input_shape_name = operation_metadata["input"]["shape"]
1511
+ if method.method_type == MethodType.CLASS.value:
1512
+ decorator = "@classmethod"
1513
+ method_args = add_indent("cls,\n", 4)
1514
+ method_args += self._generate_method_args(operation_input_shape_name)
1515
+ operation_input_args = self._generate_operation_input_args_updated(
1516
+ operation_metadata, True, resource_attributes
1517
+ )
1518
+ exclude_resource_attrs = None
1519
+ elif method.method_type == MethodType.STATIC.value:
1520
+ decorator = "@staticmethod"
1521
+ method_args = self._generate_method_args(operation_input_shape_name)
1522
+ operation_input_args = self._generate_operation_input_args_updated(
1523
+ operation_metadata, True
1524
+ )
1525
+ exclude_resource_attrs = None
1526
+ else:
1527
+ decorator = ""
1528
+ method_args = add_indent("self,\n", 4)
1529
+ method_args += (
1530
+ self._generate_method_args(operation_input_shape_name, resource_attributes) + "\n"
1531
+ )
1532
+ operation_input_args = self._generate_operation_input_args_updated(
1533
+ operation_metadata, False, resource_attributes
1534
+ )
1535
+ exclude_resource_attrs = resource_attributes
1536
+ method_args += add_indent("session: Optional[Session] = None,\n", 4)
1537
+ method_args += add_indent("region: Optional[str] = None,", 4)
1538
+
1539
+ initialize_client = INITIALIZE_CLIENT_TEMPLATE.format(service_name=method.service_name)
1540
+ if len(self.shapes[operation_input_shape_name]["members"]) != 0:
1541
+ # the method has input arguments
1542
+ serialize_operation_input = SERIALIZE_INPUT_TEMPLATE.format(
1543
+ operation_input_args=operation_input_args
1544
+ )
1545
+ call_operation_api = CALL_OPERATION_API_TEMPLATE.format(
1546
+ operation=convert_to_snake_case(method.operation_name)
1547
+ )
1548
+ else:
1549
+ # the method has no input arguments
1550
+ serialize_operation_input = ""
1551
+ call_operation_api = CALL_OPERATION_API_NO_ARG_TEMPLATE.format(
1552
+ operation=convert_to_snake_case(method.operation_name)
1553
+ )
1554
+
1555
+ if method.return_type == "None":
1556
+ return_type = "None"
1557
+ deserialize_response = ""
1558
+ return_string = None
1559
+ elif method.return_type in BASIC_RETURN_TYPES:
1560
+ return_type = f"Optional[{method.return_type}]"
1561
+ deserialize_response = DESERIALIZE_RESPONSE_TO_BASIC_TYPE_TEMPLATE
1562
+ return_string = f"Returns:\n" f" {method.return_type}\n"
1563
+ else:
1564
+ if method.return_type == "cls":
1565
+ return_type = f'Optional["{method.resource_name}"]'
1566
+ return_type_conversion = "cls"
1567
+ return_string = f"Returns:\n" f" {method.resource_name}\n"
1568
+ else:
1569
+ return_type = f"Optional[{method.return_type}]"
1570
+ return_type_conversion = method.return_type
1571
+ return_string = f"Returns:\n" f" {method.return_type}\n"
1572
+ operation_output_shape = operation_metadata["output"]["shape"]
1573
+ deserialize_response = DESERIALIZE_RESPONSE_TEMPLATE.format(
1574
+ operation_output_shape=operation_output_shape,
1575
+ return_type_conversion=return_type_conversion,
1576
+ )
1577
+
1578
+ initialize_client = INITIALIZE_CLIENT_TEMPLATE.format(service_name=method.service_name)
1579
+ if len(self.shapes[operation_input_shape_name]["members"]) != 0:
1580
+ # the method has input arguments
1581
+ serialize_operation_input = SERIALIZE_INPUT_TEMPLATE.format(
1582
+ operation_input_args=operation_input_args
1583
+ )
1584
+ call_operation_api = CALL_OPERATION_API_TEMPLATE.format(
1585
+ operation=convert_to_snake_case(method.operation_name)
1586
+ )
1587
+ else:
1588
+ # the method has no input arguments
1589
+ serialize_operation_input = ""
1590
+ call_operation_api = CALL_OPERATION_API_NO_ARG_TEMPLATE.format(
1591
+ operation=convert_to_snake_case(method.operation_name)
1592
+ )
1593
+
1594
+ # generate docstring
1595
+ docstring = self._generate_docstring(
1596
+ title=method.docstring_title,
1597
+ operation_name=method.operation_name,
1598
+ resource_name=method.resource_name,
1599
+ operation_input_shape_name=operation_input_shape_name,
1600
+ include_session_region=True,
1601
+ return_string=return_string,
1602
+ exclude_resource_attrs=exclude_resource_attrs,
1603
+ )
1604
+
1605
+ formatted_method = GENERIC_METHOD_TEMPLATE.format(
1606
+ docstring=docstring,
1607
+ decorator=decorator,
1608
+ method_name=method.method_name,
1609
+ method_args=method_args,
1610
+ return_type=return_type,
1611
+ serialize_operation_input=serialize_operation_input,
1612
+ initialize_client=initialize_client,
1613
+ call_operation_api=call_operation_api,
1614
+ deserialize_response=deserialize_response,
1615
+ )
1616
+ return formatted_method
1617
+
1618
+ def generate_additional_get_all_method(self, method: Method, resource_attributes: list):
1619
+ """Auto-Generate methods that return a list of objects.
1620
+
1621
+ Args:
1622
+ resource_name (str): The resource name.
1623
+
1624
+ Returns:
1625
+ str: The formatted method code.
1626
+ """
1627
+ # TODO: merge this with generate_get_all_method
1628
+ operation_metadata = self.operations[method.operation_name]
1629
+ operation_input_shape_name = operation_metadata["input"]["shape"]
1630
+ exclude_list = ["next_token", "max_results"]
1631
+ if method.method_type == MethodType.CLASS.value:
1632
+ decorator = "@classmethod"
1633
+ method_args = add_indent("cls,\n", 4)
1634
+ method_args += self._generate_method_args(operation_input_shape_name, exclude_list)
1635
+ operation_input_args = self._generate_operation_input_args_updated(
1636
+ operation_metadata, True, resource_attributes, exclude_list
1637
+ )
1638
+ exclude_resource_attrs = None
1639
+ else:
1640
+ decorator = ""
1641
+ method_args = add_indent("self,\n", 4)
1642
+ method_args += self._generate_method_args(
1643
+ operation_input_shape_name, exclude_list + resource_attributes
1644
+ )
1645
+ operation_input_args = self._generate_operation_input_args_updated(
1646
+ operation_metadata, False, resource_attributes, exclude_list
1647
+ )
1648
+ exclude_resource_attrs = resource_attributes
1649
+ method_args += add_indent("session: Optional[Session] = None,\n", 4)
1650
+ method_args += add_indent("region: Optional[str] = None,", 4)
1651
+
1652
+ if method.return_type == method.resource_name:
1653
+ return_type = f'ResourceIterator["{method.resource_name}"]'
1654
+ else:
1655
+ return_type = f"ResourceIterator[{method.return_type}]"
1656
+ return_string = f"Returns:\n" f" Iterator for listed {method.return_type}.\n"
1657
+
1658
+ get_list_operation_output_shape = operation_metadata["output"]["shape"]
1659
+ list_operation_output_members = self.shapes[get_list_operation_output_shape]["members"]
1660
+
1661
+ filtered_list_operation_output_members = next(
1662
+ {key: value}
1663
+ for key, value in list_operation_output_members.items()
1664
+ if key != "NextToken"
1665
+ )
1666
+ summaries_key = next(iter(filtered_list_operation_output_members))
1667
+ summaries_shape_name = filtered_list_operation_output_members[summaries_key]["shape"]
1668
+ summary_name = self.shapes[summaries_shape_name]["member"]["shape"]
1669
+
1670
+ list_method = convert_to_snake_case(method.operation_name)
1671
+
1672
+ # TODO: add rules for custom key mapping and list methods with no args
1673
+ resource_iterator_args_list = [
1674
+ "client=client",
1675
+ f"list_method='{list_method}'",
1676
+ f"summaries_key='{summaries_key}'",
1677
+ f"summary_name='{summary_name}'",
1678
+ f"resource_cls={method.return_type}",
1679
+ "list_method_kwargs=operation_input_args",
1680
+ ]
1681
+
1682
+ resource_iterator_args = ",\n".join(resource_iterator_args_list)
1683
+ resource_iterator_args = add_indent(resource_iterator_args, 8)
1684
+ serialize_operation_input = SERIALIZE_LIST_INPUT_TEMPLATE.format(
1685
+ operation_input_args=operation_input_args
1686
+ )
1687
+ initialize_client = INITIALIZE_CLIENT_TEMPLATE.format(service_name=method.service_name)
1688
+ deserialize_response = RETURN_ITERATOR_TEMPLATE.format(
1689
+ resource_iterator_args=resource_iterator_args
1690
+ )
1691
+
1692
+ # generate docstring
1693
+ docstring = self._generate_docstring(
1694
+ title=method.docstring_title,
1695
+ operation_name=method.operation_name,
1696
+ resource_name=method.resource_name,
1697
+ operation_input_shape_name=operation_input_shape_name,
1698
+ include_session_region=True,
1699
+ return_string=return_string,
1700
+ exclude_resource_attrs=exclude_resource_attrs,
1701
+ )
1702
+
1703
+ return GENERIC_METHOD_TEMPLATE.format(
1704
+ docstring=docstring,
1705
+ decorator=decorator,
1706
+ method_name=method.method_name,
1707
+ method_args=method_args,
1708
+ return_type=return_type,
1709
+ serialize_operation_input=serialize_operation_input,
1710
+ initialize_client=initialize_client,
1711
+ call_operation_api="",
1712
+ deserialize_response=deserialize_response,
1713
+ )
1714
+
1715
+ def _get_failure_reason_ref(self, resource_name: str) -> str:
1716
+ """Get the failure reason reference for a resource object.
1717
+ Args:
1718
+ resource_name (str): The resource name.
1719
+ Returns:
1720
+ str: The failure reason reference for resource object
1721
+ """
1722
+ describe_output = self.operations["Describe" + resource_name]["output"]["shape"]
1723
+ shape_members = self.shapes[describe_output]
1724
+
1725
+ for member in shape_members["members"]:
1726
+ if "FailureReason" in member or "StatusMessage" in member:
1727
+ return f"self.{convert_to_snake_case(member)}"
1728
+
1729
+ return "'(Unknown)'"
1730
+
1731
+ def generate_wait_method(self, resource_name: str) -> str:
1732
+ """Auto-Generate WAIT Method for a waitable resource.
1733
+
1734
+ Args:
1735
+ resource_name (str): The resource name.
1736
+
1737
+ Returns:
1738
+ str: The formatted Wait Method template.
1739
+ """
1740
+ resource_status_chain, resource_states = (
1741
+ self.resources_extractor.get_status_chain_and_states(resource_name)
1742
+ )
1743
+
1744
+ # Get terminal states for resource
1745
+ terminal_resource_states = []
1746
+ for state in resource_states:
1747
+ # Handles when a resource has terminal states like UpdateCompleted, CreateFailed, etc.
1748
+ # Checking lower because case is not consistent accross resources (ie, COMPLETED vs Completed)
1749
+ if any(terminal_state.lower() in state.lower() for terminal_state in TERMINAL_STATES):
1750
+ terminal_resource_states.append(state)
1751
+
1752
+ # Get resource status key path
1753
+ status_key_path = ""
1754
+ for member in resource_status_chain:
1755
+ status_key_path += f'.{convert_to_snake_case(member["name"])}'
1756
+
1757
+ failure_reason = self._get_failure_reason_ref(resource_name)
1758
+ formatted_failed_block = FAILED_STATUS_ERROR_TEMPLATE.format(
1759
+ resource_name=resource_name, reason=failure_reason
1760
+ )
1761
+ formatted_failed_block = add_indent(formatted_failed_block, 12)
1762
+
1763
+ formatted_method = WAIT_METHOD_TEMPLATE.format(
1764
+ terminal_resource_states=terminal_resource_states,
1765
+ status_key_path=status_key_path,
1766
+ failed_error_block=formatted_failed_block,
1767
+ resource_name=resource_name,
1768
+ )
1769
+ return formatted_method
1770
+
1771
+ def generate_wait_for_status_method(self, resource_name: str) -> str:
1772
+ """Auto-Generate WAIT_FOR_STATUS Method for a waitable resource.
1773
+
1774
+ Args:
1775
+ resource_name (str): The resource name.
1776
+
1777
+ Returns:
1778
+ str: The formatted wait_for_status Method template.
1779
+ """
1780
+ resource_status_chain, resource_states = (
1781
+ self.resources_extractor.get_status_chain_and_states(resource_name)
1782
+ )
1783
+
1784
+ # Get resource status key path
1785
+ status_key_path = ""
1786
+ for member in resource_status_chain:
1787
+ status_key_path += f'.{convert_to_snake_case(member["name"])}'
1788
+
1789
+ formatted_failed_block = ""
1790
+ if any("failed" in state.lower() for state in resource_states):
1791
+ failure_reason = self._get_failure_reason_ref(resource_name)
1792
+ formatted_failed_block = FAILED_STATUS_ERROR_TEMPLATE.format(
1793
+ resource_name=resource_name, reason=failure_reason
1794
+ )
1795
+ formatted_failed_block = add_indent(formatted_failed_block, 8)
1796
+
1797
+ formatted_method = WAIT_FOR_STATUS_METHOD_TEMPLATE.format(
1798
+ resource_states=resource_states,
1799
+ status_key_path=status_key_path,
1800
+ failed_error_block=formatted_failed_block,
1801
+ resource_name=resource_name,
1802
+ )
1803
+ return formatted_method
1804
+
1805
+ def generate_wait_for_delete_method(self, resource_name: str) -> str:
1806
+ """Auto-Generate WAIT_FOR_DELETE Method for a resource with deleting status.
1807
+
1808
+ Args:
1809
+ resource_name (str): The resource name.
1810
+
1811
+ Returns:
1812
+ str: The formatted wait_for_delete Method template.
1813
+ """
1814
+ resource_status_chain, resource_states = (
1815
+ self.resources_extractor.get_status_chain_and_states(resource_name)
1816
+ )
1817
+
1818
+ # Get resource status key path
1819
+ status_key_path = ""
1820
+ for member in resource_status_chain:
1821
+ status_key_path += f'.{convert_to_snake_case(member["name"])}'
1822
+
1823
+ formatted_failed_block = ""
1824
+ if any("delete_failed" in state.lower() for state in resource_states):
1825
+ failure_reason = self._get_failure_reason_ref(resource_name)
1826
+ formatted_failed_block = DELETE_FAILED_STATUS_CHECK.format(
1827
+ resource_name=resource_name, reason=failure_reason
1828
+ )
1829
+ formatted_failed_block = add_indent(formatted_failed_block, 12)
1830
+
1831
+ if any(state.lower() == "deleted" for state in resource_states):
1832
+ deleted_status_check = add_indent(DELETED_STATUS_CHECK, 12)
1833
+ else:
1834
+ deleted_status_check = ""
1835
+
1836
+ formatted_method = WAIT_FOR_DELETE_METHOD_TEMPLATE.format(
1837
+ resource_states=resource_states,
1838
+ status_key_path=status_key_path,
1839
+ delete_failed_error_block=formatted_failed_block,
1840
+ deleted_status_check=deleted_status_check,
1841
+ resource_name=resource_name,
1842
+ )
1843
+ return formatted_method
1844
+
1845
+ def generate_get_all_method(self, resource_name: str) -> str:
1846
+ """Auto-Generate 'get_all' class Method [list API] for a resource.
1847
+
1848
+ Args:
1849
+ resource_name (str): The resource name.
1850
+
1851
+ Returns:
1852
+ str: The formatted get_all Method template.
1853
+ """
1854
+ operation_name = "List" + resource_name + "s"
1855
+ operation_metadata = self.operations[operation_name]
1856
+ operation_input_shape_name = operation_metadata["input"]["shape"]
1857
+
1858
+ operation = convert_to_snake_case(operation_name)
1859
+
1860
+ get_list_operation_output_shape = self.operations[operation_name]["output"]["shape"]
1861
+ list_operation_output_members = self.shapes[get_list_operation_output_shape]["members"]
1862
+
1863
+ filtered_list_operation_output_members = next(
1864
+ {key: value}
1865
+ for key, value in list_operation_output_members.items()
1866
+ if key != "NextToken"
1867
+ )
1868
+
1869
+ summaries_key = next(iter(filtered_list_operation_output_members))
1870
+ summaries_shape_name = filtered_list_operation_output_members[summaries_key]["shape"]
1871
+
1872
+ summary_name = self.shapes[summaries_shape_name]["member"]["shape"]
1873
+ summary_members = self.shapes[summary_name]["members"].keys()
1874
+
1875
+ if "Describe" + resource_name in self.operations:
1876
+ get_operation = self.operations["Describe" + resource_name]
1877
+ get_operation_input_shape = get_operation["input"]["shape"]
1878
+ get_operation_required_input = self.shapes[get_operation_input_shape].get(
1879
+ "required", []
1880
+ )
1881
+ else:
1882
+ get_operation_required_input = []
1883
+
1884
+ custom_key_mapping_str = ""
1885
+ if any(member not in summary_members for member in get_operation_required_input):
1886
+ if "MonitoringJobDefinitionSummary" == summary_name:
1887
+ custom_key_mapping = {
1888
+ "monitoring_job_definition_name": "job_definition_name",
1889
+ "monitoring_job_definition_arn": "job_definition_arn",
1890
+ }
1891
+ custom_key_mapping_str = f"custom_key_mapping = {json.dumps(custom_key_mapping)}"
1892
+ custom_key_mapping_str = add_indent(custom_key_mapping_str, 4)
1893
+ else:
1894
+ log.warning(
1895
+ f"Resource {resource_name} summaries do not have required members to create object instance. Resource may require custom key mapping for get_all().\n"
1896
+ f"List {summary_name} Members: {summary_members}, Object Required Members: {get_operation_required_input}"
1897
+ )
1898
+ return ""
1899
+
1900
+ resource_iterator_args_list = [
1901
+ "client=client",
1902
+ f"list_method='{operation}'",
1903
+ f"summaries_key='{summaries_key}'",
1904
+ f"summary_name='{summary_name}'",
1905
+ f"resource_cls={resource_name}",
1906
+ ]
1907
+
1908
+ if custom_key_mapping_str:
1909
+ resource_iterator_args_list.append(f"custom_key_mapping=custom_key_mapping")
1910
+
1911
+ exclude_list = ["next_token", "max_results"]
1912
+ get_all_args = self._generate_method_args(operation_input_shape_name, exclude_list)
1913
+
1914
+ if not get_all_args.strip().strip(","):
1915
+ resource_iterator_args = ",\n".join(resource_iterator_args_list)
1916
+ resource_iterator_args = add_indent(resource_iterator_args, 8)
1917
+
1918
+ formatted_method = GET_ALL_METHOD_NO_ARGS_TEMPLATE.format(
1919
+ service_name="sagemaker",
1920
+ resource=resource_name,
1921
+ operation=operation,
1922
+ custom_key_mapping=custom_key_mapping_str,
1923
+ resource_iterator_args=resource_iterator_args,
1924
+ )
1925
+ return formatted_method
1926
+
1927
+ operation_input_args = self._generate_operation_input_args(
1928
+ operation_metadata, is_class_method=True, exclude_list=exclude_list
1929
+ )
1930
+
1931
+ resource_iterator_args_list.append("list_method_kwargs=operation_input_args")
1932
+ resource_iterator_args = ",\n".join(resource_iterator_args_list)
1933
+ resource_iterator_args = add_indent(resource_iterator_args, 8)
1934
+
1935
+ # generate docstring
1936
+ docstring = self._generate_docstring(
1937
+ title=f"Get all {resource_name} resources",
1938
+ operation_name=operation_name,
1939
+ resource_name=resource_name,
1940
+ operation_input_shape_name=operation_input_shape_name,
1941
+ include_session_region=True,
1942
+ include_return_resource_docstring=False,
1943
+ return_string=f"Returns:\n" f" Iterator for listed {resource_name} resources.\n",
1944
+ )
1945
+
1946
+ formatted_method = GET_ALL_METHOD_WITH_ARGS_TEMPLATE.format(
1947
+ docstring=docstring,
1948
+ service_name="sagemaker",
1949
+ resource=resource_name,
1950
+ get_all_args=get_all_args,
1951
+ operation_input_args=operation_input_args,
1952
+ custom_key_mapping=custom_key_mapping_str,
1953
+ resource_iterator_args=resource_iterator_args,
1954
+ )
1955
+ return formatted_method
1956
+
1957
+ def generate_config_schema(self):
1958
+ """
1959
+ Generates the Config Schema that is used by json Schema to validate config jsons .
1960
+ This function creates a python file with a variable that is consumed in the scripts to further fetch configs.
1961
+
1962
+ Input for generating the Schema is the service JSON that is already loaded in the class
1963
+
1964
+ """
1965
+ self.resources_extractor = ResourcesExtractor()
1966
+ self.resources_plan = self.resources_extractor.get_resource_plan()
1967
+
1968
+ resource_properties = {}
1969
+
1970
+ for _, row in self.resources_plan.iterrows():
1971
+ resource_name = row["resource_name"]
1972
+ # Get the operation and shape for the 'get' method
1973
+ if self._is_get_in_class_methods(row["class_methods"]):
1974
+ get_operation = self.operations["Describe" + resource_name]
1975
+ get_operation_shape = get_operation["output"]["shape"]
1976
+
1977
+ # Generate the class attributes based on the shape
1978
+ class_attributes = self.shapes_extractor.generate_shape_members(get_operation_shape)
1979
+ cleaned_class_attributes = self._cleanup_class_attributes_types(class_attributes)
1980
+ resource_name = row["resource_name"]
1981
+
1982
+ if default_attributes := self._get_dict_with_default_configurable_attributes(
1983
+ cleaned_class_attributes
1984
+ ):
1985
+ resource_properties[resource_name] = {
1986
+ TYPE: OBJECT,
1987
+ PROPERTIES: default_attributes,
1988
+ }
1989
+
1990
+ combined_config_schema = {
1991
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
1992
+ TYPE: OBJECT,
1993
+ PROPERTIES: {
1994
+ SCHEMA_VERSION: {
1995
+ TYPE: "string",
1996
+ "enum": ["1.0"],
1997
+ "description": "The schema version of the document.",
1998
+ },
1999
+ SAGEMAKER: {
2000
+ TYPE: OBJECT,
2001
+ PROPERTIES: {
2002
+ PYTHON_SDK: {
2003
+ TYPE: OBJECT,
2004
+ PROPERTIES: {
2005
+ RESOURCES: {
2006
+ TYPE: OBJECT,
2007
+ PROPERTIES: resource_properties,
2008
+ }
2009
+ },
2010
+ "required": [RESOURCES],
2011
+ }
2012
+ },
2013
+ "required": [PYTHON_SDK],
2014
+ },
2015
+ },
2016
+ "required": [SAGEMAKER],
2017
+ }
2018
+
2019
+ output = f"{GENERATED_CLASSES_LOCATION}/{CONFIG_SCHEMA_FILE_NAME}"
2020
+ # Open the output file
2021
+ with open(output, "w") as file:
2022
+ # Generate and write the license to the file
2023
+ file.write(
2024
+ f"SAGEMAKER_PYTHON_SDK_CONFIG_SCHEMA = {json.dumps(combined_config_schema, indent=4)}"
2025
+ )
2026
+
2027
+ def _cleanup_class_attributes_types(self, class_attributes: dict) -> dict:
2028
+ """
2029
+ Helper function that creates a direct mapping of attribute to type without default parameters assigned and without Optionals
2030
+ Args:
2031
+ class_attributes: attributes of the class in raw form
2032
+
2033
+ Returns:
2034
+ class attributes that have a direct mapping and can be used for processing
2035
+
2036
+ """
2037
+ cleaned_class_attributes = {}
2038
+ for key, value in class_attributes.items():
2039
+ new_val = value.split("=")[0].strip()
2040
+ if new_val.startswith("Optional"):
2041
+ new_val = new_val.replace("Optional[", "")[:-1]
2042
+ cleaned_class_attributes[key] = new_val
2043
+ return cleaned_class_attributes
2044
+
2045
+ def _get_dict_with_default_configurable_attributes(self, class_attributes: dict) -> dict:
2046
+ """
2047
+ Creates default attributes dict for a particular resource.
2048
+ Iterates through all class attributes and filters by attributes that have particular substrings in their name
2049
+ Args:
2050
+ class_attributes: Dict that has all the attributes of a class
2051
+
2052
+ Returns:
2053
+ Dict with attributes that can be configurable
2054
+
2055
+ """
2056
+ PYTHON_TYPES = ["str", "datetime.datetime", "bool", "int", "float"]
2057
+ default_attributes = {}
2058
+ for key, value in class_attributes.items():
2059
+ if value in PYTHON_TYPES or value.startswith("List"):
2060
+ for config_attribute_substring in CONFIGURABLE_ATTRIBUTE_SUBSTRINGS:
2061
+ if config_attribute_substring in key:
2062
+ if value.startswith("List"):
2063
+ element = value.replace("List[", "")[:-1]
2064
+ if element in PYTHON_TYPES:
2065
+ default_attributes[key] = {
2066
+ TYPE: "array",
2067
+ "items": {
2068
+ TYPE: self._get_json_schema_type_from_python_type(element)
2069
+ },
2070
+ }
2071
+ else:
2072
+ default_attributes[key] = {
2073
+ TYPE: self._get_json_schema_type_from_python_type(value) or value
2074
+ }
2075
+ elif value.startswith("List") or value.startswith("Dict"):
2076
+ log.info("Script does not currently support list of objects as configurable")
2077
+ continue
2078
+ else:
2079
+ class_attributes = self.shapes_extractor.generate_shape_members(value)
2080
+ cleaned_class_attributes = self._cleanup_class_attributes_types(class_attributes)
2081
+ if nested_default_attributes := self._get_dict_with_default_configurable_attributes(
2082
+ cleaned_class_attributes
2083
+ ):
2084
+ default_attributes[key] = nested_default_attributes
2085
+
2086
+ return default_attributes
2087
+
2088
+ def _get_json_schema_type_from_python_type(self, python_type) -> str:
2089
+ """
2090
+ Helper for generating Schema
2091
+ Converts Python Types to JSON Schema compliant string
2092
+ Args:
2093
+ python_type: Type as a string
2094
+
2095
+ Returns:
2096
+ JSON Schema compliant type
2097
+ """
2098
+ if python_type.startswith("List"):
2099
+ return "array"
2100
+ return PYTHON_TYPES_TO_BASIC_JSON_TYPES.get(python_type, None)
2101
+
2102
+ @staticmethod
2103
+ def _is_get_in_class_methods(class_methods) -> bool:
2104
+ """
2105
+ Helper to check if class methods contain Get
2106
+ Args:
2107
+ class_methods: list of methods
2108
+
2109
+ Returns:
2110
+ True if 'get' in list , else False
2111
+ """
2112
+ return "get" in class_methods
2113
+
2114
+ @staticmethod
2115
+ @lru_cache(maxsize=None)
2116
+ def _get_config_schema_for_resources():
2117
+ """
2118
+ Fetches Schema JSON for all resources from generated file
2119
+ """
2120
+ return SAGEMAKER_PYTHON_SDK_CONFIG_SCHEMA[PROPERTIES][SAGEMAKER][PROPERTIES][PYTHON_SDK][
2121
+ PROPERTIES
2122
+ ][RESOURCES][PROPERTIES]