github.com/phrase/openapi@v0.0.0-20240514140800-49e8a106740e/openapi-generator/templates/python/python-experimental/model_utils.mustache (about) 1 # coding: utf-8 2 3 {{>partial_header}} 4 5 from datetime import date, datetime # noqa: F401 6 import inspect 7 import os 8 import pprint 9 import re 10 import tempfile 11 12 from dateutil.parser import parse 13 import six 14 15 from {{packageName}}.exceptions import ( 16 ApiKeyError, 17 ApiTypeError, 18 ApiValueError, 19 ) 20 21 none_type = type(None) 22 if six.PY3: 23 import io 24 file_type = io.IOBase 25 # these are needed for when other modules import str and int from here 26 str = str 27 int = int 28 else: 29 file_type = file # noqa: F821 30 str_py2 = str 31 unicode_py2 = unicode # noqa: F821 32 long_py2 = long # noqa: F821 33 int_py2 = int 34 # this requires that the future library is installed 35 from builtins import int, str 36 37 38 class OpenApiModel(object): 39 """The base class for all OpenAPIModels""" 40 41 {{> python-experimental/model_templates/method_set_attribute }} 42 43 {{> python-experimental/model_templates/methods_shared }} 44 45 46 class ModelSimple(OpenApiModel): 47 """the parent class of models whose type != object in their 48 swagger/openapi""" 49 50 {{> python-experimental/model_templates/methods_setattr_getattr_normal }} 51 52 {{> python-experimental/model_templates/methods_tostr_eq_simple }} 53 54 55 class ModelNormal(OpenApiModel): 56 """the parent class of models whose type == object in their 57 swagger/openapi""" 58 59 {{> python-experimental/model_templates/methods_setattr_getattr_normal }} 60 61 {{> python-experimental/model_templates/methods_todict_tostr_eq_shared}} 62 63 64 class ModelComposed(OpenApiModel): 65 """the parent class of models whose type == object in their 66 swagger/openapi and have oneOf/allOf/anyOf""" 67 68 {{> python-experimental/model_templates/methods_setattr_getattr_composed }} 69 70 {{> python-experimental/model_templates/methods_todict_tostr_eq_shared}} 71 72 73 COERCION_INDEX_BY_TYPE = { 74 ModelComposed: 0, 75 ModelNormal: 1, 76 ModelSimple: 2, 77 none_type: 3, # The type of 'None'. 78 list: 4, 79 dict: 5, 80 float: 6, 81 int: 7, 82 bool: 8, 83 datetime: 9, 84 date: 10, 85 str: 11, 86 file_type: 12, # 'file_type' is an alias for the built-in 'file' or 'io.IOBase' type. 87 } 88 89 # these are used to limit what type conversions we try to do 90 # when we have a valid type already and we want to try converting 91 # to another type 92 UPCONVERSION_TYPE_PAIRS = ( 93 (str, datetime), 94 (str, date), 95 (list, ModelComposed), 96 (dict, ModelComposed), 97 (list, ModelNormal), 98 (dict, ModelNormal), 99 (str, ModelSimple), 100 (int, ModelSimple), 101 (float, ModelSimple), 102 (list, ModelSimple), 103 ) 104 105 COERCIBLE_TYPE_PAIRS = { 106 False: ( # client instantiation of a model with client data 107 # (dict, ModelComposed), 108 # (list, ModelComposed), 109 # (dict, ModelNormal), 110 # (list, ModelNormal), 111 # (str, ModelSimple), 112 # (int, ModelSimple), 113 # (float, ModelSimple), 114 # (list, ModelSimple), 115 # (str, int), 116 # (str, float), 117 # (str, datetime), 118 # (str, date), 119 # (int, str), 120 # (float, str), 121 ), 122 True: ( # server -> client data 123 (dict, ModelComposed), 124 (list, ModelComposed), 125 (dict, ModelNormal), 126 (list, ModelNormal), 127 (str, ModelSimple), 128 (int, ModelSimple), 129 (float, ModelSimple), 130 (list, ModelSimple), 131 # (str, int), 132 # (str, float), 133 (str, datetime), 134 (str, date), 135 # (int, str), 136 # (float, str), 137 (str, file_type) 138 ), 139 } 140 141 142 def get_simple_class(input_value): 143 """Returns an input_value's simple class that we will use for type checking 144 Python2: 145 float and int will return int, where int is the python3 int backport 146 str and unicode will return str, where str is the python3 str backport 147 Note: float and int ARE both instances of int backport 148 Note: str_py2 and unicode_py2 are NOT both instances of str backport 149 150 Args: 151 input_value (class/class_instance): the item for which we will return 152 the simple class 153 """ 154 if isinstance(input_value, type): 155 # input_value is a class 156 return input_value 157 elif isinstance(input_value, tuple): 158 return tuple 159 elif isinstance(input_value, list): 160 return list 161 elif isinstance(input_value, dict): 162 return dict 163 elif isinstance(input_value, none_type): 164 return none_type 165 elif isinstance(input_value, file_type): 166 return file_type 167 elif isinstance(input_value, bool): 168 # this must be higher than the int check because 169 # isinstance(True, int) == True 170 return bool 171 elif isinstance(input_value, int): 172 # for python2 input_value==long_instance -> return int 173 # where int is the python3 int backport 174 return int 175 elif isinstance(input_value, datetime): 176 # this must be higher than the date check because 177 # isinstance(datetime_instance, date) == True 178 return datetime 179 elif isinstance(input_value, date): 180 return date 181 elif (six.PY2 and isinstance(input_value, (str_py2, unicode_py2, str)) or 182 isinstance(input_value, str)): 183 return str 184 return type(input_value) 185 186 187 def check_allowed_values(allowed_values, input_variable_path, input_values): 188 """Raises an exception if the input_values are not allowed 189 190 Args: 191 allowed_values (dict): the allowed_values dict 192 input_variable_path (tuple): the path to the input variable 193 input_values (list/str/int/float/date/datetime): the values that we 194 are checking to see if they are in allowed_values 195 """ 196 these_allowed_values = list(allowed_values[input_variable_path].values()) 197 if (isinstance(input_values, list) 198 and not set(input_values).issubset( 199 set(these_allowed_values))): 200 invalid_values = ", ".join( 201 map(str, set(input_values) - set(these_allowed_values))), 202 raise ApiValueError( 203 "Invalid values for `%s` [%s], must be a subset of [%s]" % 204 ( 205 input_variable_path[0], 206 invalid_values, 207 ", ".join(map(str, these_allowed_values)) 208 ) 209 ) 210 elif (isinstance(input_values, dict) 211 and not set( 212 input_values.keys()).issubset(set(these_allowed_values))): 213 invalid_values = ", ".join( 214 map(str, set(input_values.keys()) - set(these_allowed_values))) 215 raise ApiValueError( 216 "Invalid keys in `%s` [%s], must be a subset of [%s]" % 217 ( 218 input_variable_path[0], 219 invalid_values, 220 ", ".join(map(str, these_allowed_values)) 221 ) 222 ) 223 elif (not isinstance(input_values, (list, dict)) 224 and input_values not in these_allowed_values): 225 raise ApiValueError( 226 "Invalid value for `%s` (%s), must be one of %s" % 227 ( 228 input_variable_path[0], 229 input_values, 230 these_allowed_values 231 ) 232 ) 233 234 235 def check_validations(validations, input_variable_path, input_values): 236 """Raises an exception if the input_values are invalid 237 238 Args: 239 validations (dict): the validation dictionary 240 input_variable_path (tuple): the path to the input variable 241 input_values (list/str/int/float/date/datetime): the values that we 242 are checking 243 """ 244 current_validations = validations[input_variable_path] 245 if ('max_length' in current_validations and 246 len(input_values) > current_validations['max_length']): 247 raise ApiValueError( 248 "Invalid value for `%s`, length must be less than or equal to " 249 "`%s`" % ( 250 input_variable_path[0], 251 current_validations['max_length'] 252 ) 253 ) 254 255 if ('min_length' in current_validations and 256 len(input_values) < current_validations['min_length']): 257 raise ApiValueError( 258 "Invalid value for `%s`, length must be greater than or equal to " 259 "`%s`" % ( 260 input_variable_path[0], 261 current_validations['min_length'] 262 ) 263 ) 264 265 if ('max_items' in current_validations and 266 len(input_values) > current_validations['max_items']): 267 raise ApiValueError( 268 "Invalid value for `%s`, number of items must be less than or " 269 "equal to `%s`" % ( 270 input_variable_path[0], 271 current_validations['max_items'] 272 ) 273 ) 274 275 if ('min_items' in current_validations and 276 len(input_values) < current_validations['min_items']): 277 raise ValueError( 278 "Invalid value for `%s`, number of items must be greater than or " 279 "equal to `%s`" % ( 280 input_variable_path[0], 281 current_validations['min_items'] 282 ) 283 ) 284 285 items = ('exclusive_maximum', 'inclusive_maximum', 'exclusive_minimum', 286 'inclusive_minimum') 287 if (any(item in current_validations for item in items)): 288 if isinstance(input_values, list): 289 max_val = max(input_values) 290 min_val = min(input_values) 291 elif isinstance(input_values, dict): 292 max_val = max(input_values.values()) 293 min_val = min(input_values.values()) 294 else: 295 max_val = input_values 296 min_val = input_values 297 298 if ('exclusive_maximum' in current_validations and 299 max_val >= current_validations['exclusive_maximum']): 300 raise ApiValueError( 301 "Invalid value for `%s`, must be a value less than `%s`" % ( 302 input_variable_path[0], 303 current_validations['exclusive_maximum'] 304 ) 305 ) 306 307 if ('inclusive_maximum' in current_validations and 308 max_val > current_validations['inclusive_maximum']): 309 raise ApiValueError( 310 "Invalid value for `%s`, must be a value less than or equal to " 311 "`%s`" % ( 312 input_variable_path[0], 313 current_validations['inclusive_maximum'] 314 ) 315 ) 316 317 if ('exclusive_minimum' in current_validations and 318 min_val <= current_validations['exclusive_minimum']): 319 raise ApiValueError( 320 "Invalid value for `%s`, must be a value greater than `%s`" % 321 ( 322 input_variable_path[0], 323 current_validations['exclusive_maximum'] 324 ) 325 ) 326 327 if ('inclusive_minimum' in current_validations and 328 min_val < current_validations['inclusive_minimum']): 329 raise ApiValueError( 330 "Invalid value for `%s`, must be a value greater than or equal " 331 "to `%s`" % ( 332 input_variable_path[0], 333 current_validations['inclusive_minimum'] 334 ) 335 ) 336 flags = current_validations.get('regex', {}).get('flags', 0) 337 if ('regex' in current_validations and 338 not re.search(current_validations['regex']['pattern'], 339 input_values, flags=flags)): 340 raise ApiValueError( 341 r"Invalid value for `%s`, must be a follow pattern or equal to " 342 r"`%s` with flags=`%s`" % ( 343 input_variable_path[0], 344 current_validations['regex']['pattern'], 345 flags 346 ) 347 ) 348 349 350 def order_response_types(required_types): 351 """Returns the required types sorted in coercion order 352 353 Args: 354 required_types (list/tuple): collection of classes or instance of 355 list or dict with class information inside it. 356 357 Returns: 358 (list): coercion order sorted collection of classes or instance 359 of list or dict with class information inside it. 360 """ 361 362 def index_getter(class_or_instance): 363 if isinstance(class_or_instance, list): 364 return COERCION_INDEX_BY_TYPE[list] 365 elif isinstance(class_or_instance, dict): 366 return COERCION_INDEX_BY_TYPE[dict] 367 elif (inspect.isclass(class_or_instance) 368 and issubclass(class_or_instance, ModelComposed)): 369 return COERCION_INDEX_BY_TYPE[ModelComposed] 370 elif (inspect.isclass(class_or_instance) 371 and issubclass(class_or_instance, ModelNormal)): 372 return COERCION_INDEX_BY_TYPE[ModelNormal] 373 elif (inspect.isclass(class_or_instance) 374 and issubclass(class_or_instance, ModelSimple)): 375 return COERCION_INDEX_BY_TYPE[ModelSimple] 376 elif class_or_instance in COERCION_INDEX_BY_TYPE: 377 return COERCION_INDEX_BY_TYPE[class_or_instance] 378 raise ApiValueError("Unsupported type: %s" % class_or_instance) 379 380 sorted_types = sorted( 381 required_types, 382 key=lambda class_or_instance: index_getter(class_or_instance) 383 ) 384 return sorted_types 385 386 387 def remove_uncoercible(required_types_classes, current_item, from_server, 388 must_convert=True): 389 """Only keeps the type conversions that are possible 390 391 Args: 392 required_types_classes (tuple): tuple of classes that are required 393 these should be ordered by COERCION_INDEX_BY_TYPE 394 from_server (bool): a boolean of whether the data is from the server 395 if false, the data is from the client 396 current_item (any): the current item (input data) to be converted 397 398 Keyword Args: 399 must_convert (bool): if True the item to convert is of the wrong 400 type and we want a big list of coercibles 401 if False, we want a limited list of coercibles 402 403 Returns: 404 (list): the remaining coercible required types, classes only 405 """ 406 current_type_simple = get_simple_class(current_item) 407 408 results_classes = [] 409 for required_type_class in required_types_classes: 410 # convert our models to OpenApiModel 411 required_type_class_simplified = required_type_class 412 if isinstance(required_type_class_simplified, type): 413 if issubclass(required_type_class_simplified, ModelComposed): 414 required_type_class_simplified = ModelComposed 415 elif issubclass(required_type_class_simplified, ModelNormal): 416 required_type_class_simplified = ModelNormal 417 elif issubclass(required_type_class_simplified, ModelSimple): 418 required_type_class_simplified = ModelSimple 419 420 if required_type_class_simplified == current_type_simple: 421 # don't consider converting to one's own class 422 continue 423 424 class_pair = (current_type_simple, required_type_class_simplified) 425 if must_convert and class_pair in COERCIBLE_TYPE_PAIRS[from_server]: 426 results_classes.append(required_type_class) 427 elif class_pair in UPCONVERSION_TYPE_PAIRS: 428 results_classes.append(required_type_class) 429 return results_classes 430 431 432 def get_required_type_classes(required_types_mixed): 433 """Converts the tuple required_types into a tuple and a dict described 434 below 435 436 Args: 437 required_types_mixed (tuple/list): will contain either classes or 438 instance of list or dict 439 440 Returns: 441 (valid_classes, dict_valid_class_to_child_types_mixed): 442 valid_classes (tuple): the valid classes that the current item 443 should be 444 dict_valid_class_to_child_types_mixed (doct): 445 valid_class (class): this is the key 446 child_types_mixed (list/dict/tuple): describes the valid child 447 types 448 """ 449 valid_classes = [] 450 child_req_types_by_current_type = {} 451 for required_type in required_types_mixed: 452 if isinstance(required_type, list): 453 valid_classes.append(list) 454 child_req_types_by_current_type[list] = required_type 455 elif isinstance(required_type, tuple): 456 valid_classes.append(tuple) 457 child_req_types_by_current_type[tuple] = required_type 458 elif isinstance(required_type, dict): 459 valid_classes.append(dict) 460 child_req_types_by_current_type[dict] = required_type[str] 461 else: 462 valid_classes.append(required_type) 463 return tuple(valid_classes), child_req_types_by_current_type 464 465 466 def change_keys_js_to_python(input_dict, model_class): 467 """ 468 Converts from javascript_key keys in the input_dict to python_keys in 469 the output dict using the mapping in model_class. 470 If the input_dict contains a key which does not declared in the model_class, 471 the key is added to the output dict as is. The assumption is the model_class 472 may have undeclared properties (additionalProperties attribute in the OAS 473 document). 474 """ 475 476 output_dict = {} 477 reversed_attr_map = {value: key for key, value in 478 six.iteritems(model_class.attribute_map)} 479 for javascript_key, value in six.iteritems(input_dict): 480 python_key = reversed_attr_map.get(javascript_key) 481 if python_key is None: 482 # if the key is unknown, it is in error or it is an 483 # additionalProperties variable 484 python_key = javascript_key 485 output_dict[python_key] = value 486 return output_dict 487 488 489 def get_type_error(var_value, path_to_item, valid_classes, key_type=False): 490 error_msg = type_error_message( 491 var_name=path_to_item[-1], 492 var_value=var_value, 493 valid_classes=valid_classes, 494 key_type=key_type 495 ) 496 return ApiTypeError( 497 error_msg, 498 path_to_item=path_to_item, 499 valid_classes=valid_classes, 500 key_type=key_type 501 ) 502 503 504 def deserialize_primitive(data, klass, path_to_item): 505 """Deserializes string to primitive type. 506 507 :param data: str/int/float 508 :param klass: str/class the class to convert to 509 510 :return: int, float, str, bool, date, datetime 511 """ 512 additional_message = "" 513 try: 514 if klass in {datetime, date}: 515 additional_message = ( 516 "If you need your parameter to have a fallback " 517 "string value, please set its type as `type: {}` in your " 518 "spec. That allows the value to be any type. " 519 ) 520 if klass == datetime: 521 if len(data) < 8: 522 raise ValueError("This is not a datetime") 523 # The string should be in iso8601 datetime format. 524 parsed_datetime = parse(data) 525 date_only = ( 526 parsed_datetime.hour == 0 and 527 parsed_datetime.minute == 0 and 528 parsed_datetime.second == 0 and 529 parsed_datetime.tzinfo is None and 530 8 <= len(data) <= 10 531 ) 532 if date_only: 533 raise ValueError("This is a date, not a datetime") 534 return parsed_datetime 535 elif klass == date: 536 if len(data) < 8: 537 raise ValueError("This is not a date") 538 return parse(data).date() 539 else: 540 converted_value = klass(data) 541 if isinstance(data, str) and klass == float: 542 if str(converted_value) != data: 543 # '7' -> 7.0 -> '7.0' != '7' 544 raise ValueError('This is not a float') 545 return converted_value 546 except (OverflowError, ValueError): 547 # parse can raise OverflowError 548 raise ApiValueError( 549 "{0}Failed to parse {1} as {2}".format( 550 additional_message, repr(data), get_py3_class_name(klass) 551 ), 552 path_to_item=path_to_item 553 ) 554 555 556 def fix_model_input_data(model_data, model_class): 557 # this is only called on classes where the input data is a dict 558 fixed_model_data = change_keys_js_to_python( 559 model_data, 560 model_class 561 ) 562 if model_class._composed_schemas() is not None: 563 for allof_class in model_class._composed_schemas()['allOf']: 564 fixed_model_data = change_keys_js_to_python( 565 fixed_model_data, 566 allof_class 567 ) 568 return fixed_model_data 569 570 571 def deserialize_model(model_data, model_class, path_to_item, check_type, 572 configuration, from_server): 573 """Deserializes model_data to model instance. 574 575 Args: 576 model_data (list/dict): data to instantiate the model 577 model_class (OpenApiModel): the model class 578 path_to_item (list): path to the model in the received data 579 check_type (bool): whether to check the data tupe for the values in 580 the model 581 configuration (Configuration): the instance to use to convert files 582 from_server (bool): True if the data is from the server 583 False if the data is from the client 584 585 Returns: 586 model instance 587 588 Raise: 589 ApiTypeError 590 ApiValueError 591 ApiKeyError 592 """ 593 594 kw_args = dict(_check_type=check_type, 595 _path_to_item=path_to_item, 596 _configuration=configuration, 597 _from_server=from_server) 598 599 used_model_class = model_class 600 if model_class.discriminator() is not None: 601 used_model_class = model_class.get_discriminator_class( 602 from_server, model_data) 603 604 if issubclass(used_model_class, ModelSimple): 605 instance = used_model_class(value=model_data, **kw_args) 606 return instance 607 if isinstance(model_data, list): 608 instance = used_model_class(*model_data, **kw_args) 609 if isinstance(model_data, dict): 610 fixed_model_data = change_keys_js_to_python( 611 model_data, 612 used_model_class 613 ) 614 kw_args.update(fixed_model_data) 615 instance = used_model_class(**kw_args) 616 return instance 617 618 619 def deserialize_file(response_data, configuration, content_disposition=None): 620 """Deserializes body to file 621 622 Saves response body into a file in a temporary folder, 623 using the filename from the `Content-Disposition` header if provided. 624 625 Args: 626 param response_data (str): the file data to write 627 configuration (Configuration): the instance to use to convert files 628 629 Keyword Args: 630 content_disposition (str): the value of the Content-Disposition 631 header 632 633 Returns: 634 (file_type): the deserialized file which is open 635 The user is responsible for closing and reading the file 636 """ 637 fd, path = tempfile.mkstemp(dir=configuration.temp_folder_path) 638 os.close(fd) 639 os.remove(path) 640 641 if content_disposition: 642 filename = re.search(r'filename=[\'"]?([^\'"\s]+)[\'"]?', 643 content_disposition).group(1) 644 path = os.path.join(os.path.dirname(path), filename) 645 646 with open(path, "wb") as f: 647 if six.PY3 and isinstance(response_data, str): 648 # in python3 change str to bytes so we can write it 649 response_data = response_data.encode('utf-8') 650 f.write(response_data) 651 652 f = open(path, "rb") 653 return f 654 655 656 def attempt_convert_item(input_value, valid_classes, path_to_item, 657 configuration, from_server, key_type=False, 658 must_convert=False, check_type=True): 659 """ 660 Args: 661 input_value (any): the data to convert 662 valid_classes (any): the classes that are valid 663 path_to_item (list): the path to the item to convert 664 configuration (Configuration): the instance to use to convert files 665 from_server (bool): True if data is from the server, False is data is 666 from the client 667 key_type (bool): if True we need to convert a key type (not supported) 668 must_convert (bool): if True we must convert 669 check_type (bool): if True we check the type or the returned data in 670 ModelComposed/ModelNormal/ModelSimple instances 671 672 Returns: 673 instance (any) the fixed item 674 675 Raises: 676 ApiTypeError 677 ApiValueError 678 ApiKeyError 679 """ 680 valid_classes_ordered = order_response_types(valid_classes) 681 valid_classes_coercible = remove_uncoercible( 682 valid_classes_ordered, input_value, from_server) 683 if not valid_classes_coercible or key_type: 684 # we do not handle keytype errors, json will take care 685 # of this for us 686 if configuration is None or not configuration.discard_unknown_keys: 687 raise get_type_error(input_value, path_to_item, valid_classes, 688 key_type=key_type) 689 for valid_class in valid_classes_coercible: 690 try: 691 if issubclass(valid_class, OpenApiModel): 692 return deserialize_model(input_value, valid_class, 693 path_to_item, check_type, 694 configuration, from_server) 695 elif valid_class == file_type: 696 return deserialize_file(input_value, configuration) 697 return deserialize_primitive(input_value, valid_class, 698 path_to_item) 699 except (ApiTypeError, ApiValueError, ApiKeyError) as conversion_exc: 700 if must_convert: 701 raise conversion_exc 702 # if we have conversion errors when must_convert == False 703 # we ignore the exception and move on to the next class 704 continue 705 # we were unable to convert, must_convert == False 706 return input_value 707 708 709 def validate_and_convert_types(input_value, required_types_mixed, path_to_item, 710 from_server, _check_type, configuration=None): 711 """Raises a TypeError is there is a problem, otherwise returns value 712 713 Args: 714 input_value (any): the data to validate/convert 715 required_types_mixed (list/dict/tuple): A list of 716 valid classes, or a list tuples of valid classes, or a dict where 717 the value is a tuple of value classes 718 path_to_item: (list) the path to the data being validated 719 this stores a list of keys or indices to get to the data being 720 validated 721 from_server (bool): True if data is from the server 722 False if data is from the client 723 _check_type: (boolean) if true, type will be checked and conversion 724 will be attempted. 725 configuration: (Configuration): the configuration class to use 726 when converting file_type items. 727 If passed, conversion will be attempted when possible 728 If not passed, no conversions will be attempted and 729 exceptions will be raised 730 731 Returns: 732 the correctly typed value 733 734 Raises: 735 ApiTypeError 736 """ 737 results = get_required_type_classes(required_types_mixed) 738 valid_classes, child_req_types_by_current_type = results 739 740 input_class_simple = get_simple_class(input_value) 741 valid_type = input_class_simple in set(valid_classes) 742 if not valid_type: 743 if configuration: 744 # if input_value is not valid_type try to convert it 745 converted_instance = attempt_convert_item( 746 input_value, 747 valid_classes, 748 path_to_item, 749 configuration, 750 from_server, 751 key_type=False, 752 must_convert=True 753 ) 754 return converted_instance 755 else: 756 raise get_type_error(input_value, path_to_item, valid_classes, 757 key_type=False) 758 759 # input_value's type is in valid_classes 760 if len(valid_classes) > 1 and configuration: 761 # there are valid classes which are not the current class 762 valid_classes_coercible = remove_uncoercible( 763 valid_classes, input_value, from_server, must_convert=False) 764 if valid_classes_coercible: 765 converted_instance = attempt_convert_item( 766 input_value, 767 valid_classes_coercible, 768 path_to_item, 769 configuration, 770 from_server, 771 key_type=False, 772 must_convert=False 773 ) 774 return converted_instance 775 776 if child_req_types_by_current_type == {}: 777 # all types are of the required types and there are no more inner 778 # variables left to look at 779 return input_value 780 inner_required_types = child_req_types_by_current_type.get( 781 type(input_value) 782 ) 783 if inner_required_types is None: 784 # for this type, there are not more inner variables left to look at 785 return input_value 786 if isinstance(input_value, list): 787 if input_value == []: 788 # allow an empty list 789 return input_value 790 for index, inner_value in enumerate(input_value): 791 inner_path = list(path_to_item) 792 inner_path.append(index) 793 input_value[index] = validate_and_convert_types( 794 inner_value, 795 inner_required_types, 796 inner_path, 797 from_server, 798 _check_type, 799 configuration=configuration 800 ) 801 elif isinstance(input_value, dict): 802 if input_value == {}: 803 # allow an empty dict 804 return input_value 805 for inner_key, inner_val in six.iteritems(input_value): 806 inner_path = list(path_to_item) 807 inner_path.append(inner_key) 808 if get_simple_class(inner_key) != str: 809 raise get_type_error(inner_key, inner_path, valid_classes, 810 key_type=True) 811 input_value[inner_key] = validate_and_convert_types( 812 inner_val, 813 inner_required_types, 814 inner_path, 815 from_server, 816 _check_type, 817 configuration=configuration 818 ) 819 return input_value 820 821 822 def model_to_dict(model_instance, serialize=True): 823 """Returns the model properties as a dict 824 825 Args: 826 model_instance (one of your model instances): the model instance that 827 will be converted to a dict. 828 829 Keyword Args: 830 serialize (bool): if True, the keys in the dict will be values from 831 attribute_map 832 """ 833 result = {} 834 835 model_instances = [model_instance] 836 if model_instance._composed_schemas() is not None: 837 model_instances.extend(model_instance._composed_instances) 838 for model_instance in model_instances: 839 for attr, value in six.iteritems(model_instance._data_store): 840 if serialize: 841 # we use get here because additional property key names do not 842 # exist in attribute_map 843 attr = model_instance.attribute_map.get(attr, attr) 844 if isinstance(value, list): 845 result[attr] = list(map( 846 lambda x: model_to_dict(x, serialize=serialize) 847 if hasattr(x, '_data_store') else x, value 848 )) 849 elif isinstance(value, dict): 850 result[attr] = dict(map( 851 lambda item: (item[0], 852 model_to_dict(item[1], serialize=serialize)) 853 if hasattr(item[1], '_data_store') else item, 854 value.items() 855 )) 856 elif hasattr(value, '_data_store'): 857 result[attr] = model_to_dict(value, serialize=serialize) 858 else: 859 result[attr] = value 860 861 return result 862 863 864 def type_error_message(var_value=None, var_name=None, valid_classes=None, 865 key_type=None): 866 """ 867 Keyword Args: 868 var_value (any): the variable which has the type_error 869 var_name (str): the name of the variable which has the typ error 870 valid_classes (tuple): the accepted classes for current_item's 871 value 872 key_type (bool): False if our value is a value in a dict 873 True if it is a key in a dict 874 False if our item is an item in a list 875 """ 876 key_or_value = 'value' 877 if key_type: 878 key_or_value = 'key' 879 valid_classes_phrase = get_valid_classes_phrase(valid_classes) 880 msg = ( 881 "Invalid type for variable '{0}'. Required {1} type {2} and " 882 "passed type was {3}".format( 883 var_name, 884 key_or_value, 885 valid_classes_phrase, 886 type(var_value).__name__, 887 ) 888 ) 889 return msg 890 891 892 def get_valid_classes_phrase(input_classes): 893 """Returns a string phrase describing what types are allowed 894 Note: Adds the extra valid classes in python2 895 """ 896 all_classes = list(input_classes) 897 if six.PY2 and str in input_classes: 898 all_classes.extend([str_py2, unicode_py2]) 899 if six.PY2 and int in input_classes: 900 all_classes.extend([int_py2, long_py2]) 901 all_classes = sorted(all_classes, key=lambda cls: cls.__name__) 902 all_class_names = [cls.__name__ for cls in all_classes] 903 if len(all_class_names) == 1: 904 return 'is {0}'.format(all_class_names[0]) 905 return "is one of [{0}]".format(", ".join(all_class_names)) 906 907 908 def get_py3_class_name(input_class): 909 if six.PY2: 910 if input_class == str: 911 return 'str' 912 elif input_class == int: 913 return 'int' 914 return input_class.__name__ 915 916 917 def get_allof_instances(self, model_args, constant_args): 918 """ 919 Args: 920 self: the class we are handling 921 model_args (dict): var_name to var_value 922 used to make instances 923 constant_args (dict): var_name to var_value 924 used to make instances 925 926 Returns 927 composed_instances (list) 928 """ 929 composed_instances = [] 930 for allof_class in self._composed_schemas()['allOf']: 931 932 # transform js keys to python keys in fixed_model_args 933 fixed_model_args = change_keys_js_to_python( 934 model_args, allof_class) 935 936 # extract a dict of only required keys from fixed_model_args 937 kwargs = {} 938 var_names = set(allof_class.openapi_types().keys()) 939 for var_name in var_names: 940 if var_name in fixed_model_args: 941 kwargs[var_name] = fixed_model_args[var_name] 942 943 # and use it to make the instance 944 kwargs.update(constant_args) 945 try: 946 allof_instance = allof_class(**kwargs) 947 composed_instances.append(allof_instance) 948 except Exception as ex: 949 raise ApiValueError( 950 "Invalid inputs given to generate an instance of '%s'. The " 951 "input data was invalid for the allOf schema '%s' in the composed " 952 "schema '%s'. Error=%s" % ( 953 allof_class.__class__.__name__, 954 allof_class.__class__.__name__, 955 self.__class__.__name__, 956 str(ex) 957 ) 958 ) 959 return composed_instances 960 961 962 def get_oneof_instance(self, model_args, constant_args): 963 """ 964 Find the oneOf schema that matches the input data (e.g. payload). 965 If exactly one schema matches the input data, an instance of that schema 966 is returned. 967 If zero or more than one schema match the input data, an exception is raised. 968 In OAS 3.x, the payload MUST, by validation, match exactly one of the 969 schemas described by oneOf. 970 Args: 971 self: the class we are handling 972 model_args (dict): var_name to var_value 973 The input data, e.g. the payload that must match a oneOf schema 974 in the OpenAPI document. 975 constant_args (dict): var_name to var_value 976 args that every model requires, including configuration, server 977 and path to item. 978 979 Returns 980 oneof_instance (instance/None) 981 """ 982 if len(self._composed_schemas()['oneOf']) == 0: 983 return None 984 985 oneof_instances = [] 986 # Iterate over each oneOf schema and determine if the input data 987 # matches the oneOf schemas. 988 for oneof_class in self._composed_schemas()['oneOf']: 989 # transform js keys from input data to python keys in fixed_model_args 990 fixed_model_args = change_keys_js_to_python( 991 model_args, oneof_class) 992 993 # Extract a dict with the properties that are declared in the oneOf schema. 994 # Undeclared properties (e.g. properties that are allowed because of the 995 # additionalProperties attribute in the OAS document) are not added to 996 # the dict. 997 kwargs = {} 998 var_names = set(oneof_class.openapi_types().keys()) 999 for var_name in var_names: 1000 if var_name in fixed_model_args: 1001 kwargs[var_name] = fixed_model_args[var_name] 1002 1003 # do not try to make a model with no input args 1004 if len(kwargs) == 0: 1005 continue 1006 1007 # and use it to make the instance 1008 kwargs.update(constant_args) 1009 try: 1010 oneof_instance = oneof_class(**kwargs) 1011 oneof_instances.append(oneof_instance) 1012 except Exception: 1013 pass 1014 if len(oneof_instances) == 0: 1015 raise ApiValueError( 1016 "Invalid inputs given to generate an instance of %s. None " 1017 "of the oneOf schemas matched the input data." % 1018 self.__class__.__name__ 1019 ) 1020 elif len(oneof_instances) > 1: 1021 raise ApiValueError( 1022 "Invalid inputs given to generate an instance of %s. Multiple " 1023 "oneOf schemas matched the inputs, but a max of one is allowed." % 1024 self.__class__.__name__ 1025 ) 1026 return oneof_instances[0] 1027 1028 1029 def get_anyof_instances(self, model_args, constant_args): 1030 """ 1031 Args: 1032 self: the class we are handling 1033 model_args (dict): var_name to var_value 1034 used to make instances 1035 constant_args (dict): var_name to var_value 1036 used to make instances 1037 1038 Returns 1039 anyof_instances (list) 1040 """ 1041 anyof_instances = [] 1042 if len(self._composed_schemas()['anyOf']) == 0: 1043 return anyof_instances 1044 1045 for anyof_class in self._composed_schemas()['anyOf']: 1046 # transform js keys to python keys in fixed_model_args 1047 fixed_model_args = change_keys_js_to_python(model_args, anyof_class) 1048 1049 # extract a dict of only required keys from these_model_vars 1050 kwargs = {} 1051 var_names = set(anyof_class.openapi_types().keys()) 1052 for var_name in var_names: 1053 if var_name in fixed_model_args: 1054 kwargs[var_name] = fixed_model_args[var_name] 1055 1056 # do not try to make a model with no input args 1057 if len(kwargs) == 0: 1058 continue 1059 1060 # and use it to make the instance 1061 kwargs.update(constant_args) 1062 try: 1063 anyof_instance = anyof_class(**kwargs) 1064 anyof_instances.append(anyof_instance) 1065 except Exception: 1066 pass 1067 if len(anyof_instances) == 0: 1068 raise ApiValueError( 1069 "Invalid inputs given to generate an instance of %s. None of the " 1070 "anyOf schemas matched the inputs." % 1071 self.__class__.__name__ 1072 ) 1073 return anyof_instances 1074 1075 1076 def get_additional_properties_model_instances( 1077 composed_instances, self): 1078 additional_properties_model_instances = [] 1079 all_instances = [self] 1080 all_instances.extend(composed_instances) 1081 for instance in all_instances: 1082 if instance.additional_properties_type is not None: 1083 additional_properties_model_instances.append(instance) 1084 return additional_properties_model_instances 1085 1086 1087 def get_var_name_to_model_instances(self, composed_instances): 1088 var_name_to_model_instances = {} 1089 all_instances = [self] 1090 all_instances.extend(composed_instances) 1091 for instance in all_instances: 1092 for var_name in instance.openapi_types(): 1093 if var_name not in var_name_to_model_instances: 1094 var_name_to_model_instances[var_name] = [instance] 1095 else: 1096 var_name_to_model_instances[var_name].append(instance) 1097 return var_name_to_model_instances 1098 1099 1100 def get_unused_args(self, composed_instances, model_args): 1101 unused_args = dict(model_args) 1102 # arguments apssed to self were already converted to python names 1103 # before __init__ was called 1104 for var_name_py in self.attribute_map: 1105 if var_name_py in unused_args: 1106 del unused_args[var_name_py] 1107 for instance in composed_instances: 1108 if instance.__class__ in self._composed_schemas()['allOf']: 1109 for var_name_py in instance.attribute_map: 1110 if var_name_py in unused_args: 1111 del unused_args[var_name_py] 1112 else: 1113 for var_name_js in instance.attribute_map.values(): 1114 if var_name_js in unused_args: 1115 del unused_args[var_name_js] 1116 return unused_args 1117 1118 1119 def validate_get_composed_info(constant_args, model_args, self): 1120 """ 1121 For composed schemas, generate schema instances for 1122 all schemas in the oneOf/anyOf/allOf definition. If additional 1123 properties are allowed, also assign those properties on 1124 all matched schemas that contain additionalProperties. 1125 Openapi schemas are python classes. 1126 1127 Exceptions are raised if: 1128 - no oneOf schema matches the model_args input data 1129 - > 1 oneOf schema matches the model_args input data 1130 - > 1 oneOf schema matches the model_args input data 1131 - no anyOf schema matches the model_args input data 1132 - any of the allOf schemas do not match the model_args input data 1133 1134 Args: 1135 constant_args (dict): these are the args that every model requires 1136 model_args (dict): these are the required and optional spec args that 1137 were passed in to make this model 1138 self (class): the class that we are instantiating 1139 This class contains self._composed_schemas() 1140 1141 Returns: 1142 composed_info (list): length three 1143 composed_instances (list): the composed instances which are not 1144 self 1145 var_name_to_model_instances (dict): a dict going from var_name 1146 to the model_instance which holds that var_name 1147 the model_instance may be self or an instance of one of the 1148 classes in self.composed_instances() 1149 additional_properties_model_instances (list): a list of the 1150 model instances which have the property 1151 additional_properties_type. This list can include self 1152 """ 1153 # create composed_instances 1154 composed_instances = [] 1155 allof_instances = get_allof_instances(self, model_args, constant_args) 1156 composed_instances.extend(allof_instances) 1157 oneof_instance = get_oneof_instance(self, model_args, constant_args) 1158 if oneof_instance is not None: 1159 composed_instances.append(oneof_instance) 1160 anyof_instances = get_anyof_instances(self, model_args, constant_args) 1161 composed_instances.extend(anyof_instances) 1162 1163 # map variable names to composed_instances 1164 var_name_to_model_instances = get_var_name_to_model_instances( 1165 self, composed_instances) 1166 1167 # set additional_properties_model_instances 1168 additional_properties_model_instances = ( 1169 get_additional_properties_model_instances(composed_instances, self) 1170 ) 1171 1172 # set any remaining values 1173 unused_args = get_unused_args(self, composed_instances, model_args) 1174 if len(unused_args) > 0 and \ 1175 len(additional_properties_model_instances) == 0 and \ 1176 (self._configuration is None or 1177 not self._configuration.discard_unknown_keys): 1178 raise ApiValueError( 1179 "Invalid input arguments input when making an instance of " 1180 "class %s. Not all inputs were used. The unused input data " 1181 "is %s" % (self.__class__.__name__, unused_args) 1182 ) 1183 1184 # no need to add additional_properties to var_name_to_model_instances here 1185 # because additional_properties_model_instances will direct us to that 1186 # instance when we use getattr or setattr 1187 # and we update var_name_to_model_instances in setattr 1188 1189 return [ 1190 composed_instances, 1191 var_name_to_model_instances, 1192 additional_properties_model_instances, 1193 unused_args 1194 ]