From acd1b703f7be26bcbbc7353d244c03f16e2f2796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zejd=20=C4=8Ci=C4=8Dak?= Date: Tue, 3 Feb 2026 16:19:00 +0100 Subject: [PATCH] fix: unknown enum values --- openapitools.json | 4 +- templates/model_enum.mustache | 34 +++ templates/model_generic.mustache | 402 +++++++++++++++++++++++++++++++ 3 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 templates/model_enum.mustache create mode 100644 templates/model_generic.mustache diff --git a/openapitools.json b/openapitools.json index a704e30..4889da9 100644 --- a/openapitools.json +++ b/openapitools.json @@ -5,13 +5,15 @@ "generatorName": "python", "inputSpec": "public_accessa/api/v2/docs/cb-v2-openapi-3.0.0.yaml", "outputDir": ".", + "templateDir": "templates", "additionalProperties": { "packageName": "cloudbeds_pms", "projectName": "Cloudbeds PMS", "packageVersion": "2.9.0", "packageDescription": "OpenAPI client for Cloudbeds PMS API.", "generateSourceCodeOnly": true, - "packageUrl": "https://github.com/cloudbeds/cloudbeds-api-python" + "packageUrl": "https://github.com/cloudbeds/cloudbeds-api-python", + "enumUnknownDefaultCase": true }, "files": { "README_onlypackage.mustache": { diff --git a/templates/model_enum.mustache b/templates/model_enum.mustache new file mode 100644 index 0000000..b393851 --- /dev/null +++ b/templates/model_enum.mustache @@ -0,0 +1,34 @@ +from __future__ import annotations +import json +from enum import Enum +{{#vendorExtensions.x-py-other-imports}} +{{{.}}} +{{/vendorExtensions.x-py-other-imports}} +from typing_extensions import Self + + +class {{classname}}({{vendorExtensions.x-py-enum-type}}, Enum): + """ + {{{description}}}{{^description}}{{{classname}}}{{/description}} + """ + + """ + allowed enum values + """ +{{#allowableValues}} + {{#enumVars}} + {{{name}}} = {{{value}}} + {{/enumVars}} + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of {{classname}} from a JSON string""" + return cls(json.loads(json_str)) + +{{#enumUnknownDefaultCase}} + @classmethod + def _missing_(cls, value): + """Handle unknown enum values by returning the unknown default case.""" + return cls.UNKNOWN_DEFAULT_OPEN_API +{{/enumUnknownDefaultCase}} +{{/allowableValues}} \ No newline at end of file diff --git a/templates/model_generic.mustache b/templates/model_generic.mustache new file mode 100644 index 0000000..c3bcb0d --- /dev/null +++ b/templates/model_generic.mustache @@ -0,0 +1,402 @@ +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +{{#vendorExtensions.x-py-other-imports}} +{{{.}}} +{{/vendorExtensions.x-py-other-imports}} +{{#vendorExtensions.x-py-model-imports}} +{{{.}}} +{{/vendorExtensions.x-py-model-imports}} +from typing import Optional, Set +from typing_extensions import Self + +{{#hasChildren}} +{{#discriminator}} +{{! If this model is a super class, importlib is used. So import the necessary modules for the type here. }} +from typing import TYPE_CHECKING +if TYPE_CHECKING: +{{#mappedModels}} + from {{packageName}}.models.{{model.classFilename}} import {{modelName}} +{{/mappedModels}} + +{{/discriminator}} +{{/hasChildren}} +class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}): + """ + {{#description}}{{{description}}}{{/description}}{{^description}}{{{classname}}}{{/description}} + """ # noqa: E501 +{{#vars}} + {{name}}: {{{vendorExtensions.x-py-typing}}} +{{/vars}} +{{#isAdditionalPropertiesTrue}} + additional_properties: Dict[str, Any] = {} +{{/isAdditionalPropertiesTrue}} + __properties: ClassVar[List[str]] = [{{#allVars}}"{{baseName}}"{{^-last}}, {{/-last}}{{/allVars}}] +{{#vars}} + {{#vendorExtensions.x-regex}} + + @field_validator('{{{name}}}') + def {{{name}}}_validate_regular_expression(cls, value): + """Validates the regular expression""" + {{^required}} + if value is None: + return value + + {{/required}} + {{#required}} + {{#isNullable}} + if value is None: + return value + + {{/isNullable}} + {{/required}} + if not re.match(r"{{{.}}}", value{{#vendorExtensions.x-modifiers}} ,re.{{{.}}}{{/vendorExtensions.x-modifiers}}): + raise ValueError(r"must validate the regular expression {{{vendorExtensions.x-pattern}}}") + return value + {{/vendorExtensions.x-regex}} + {{#isEnum}} + + @field_validator('{{{name}}}') + def {{{name}}}_validate_enum(cls, value): + """Validates the enum, returning unknown_default_open_api for unrecognized values""" + {{^required}} + if value is None: + return value + + {{/required}} + {{#required}} + {{#isNullable}} + if value is None: + return value + + {{/isNullable}} + {{/required}} + _allowed_values = set([{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]) + {{#isContainer}} + {{#isArray}} + # Map unknown values to the fallback + return [i if i in _allowed_values else 'unknown_default_open_api' for i in value] + {{/isArray}} + {{#isMap}} + # Map unknown values to the fallback + return {k: (v if v in _allowed_values else 'unknown_default_open_api') for k, v in value.items()} + {{/isMap}} + {{/isContainer}} + {{^isContainer}} + if value not in _allowed_values: + return 'unknown_default_open_api' + {{/isContainer}} + return value + {{/isEnum}} +{{/vars}} + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + +{{#hasChildren}} +{{#discriminator}} + # JSON field name that stores the object type + __discriminator_property_name: ClassVar[str] = '{{discriminator.propertyBaseName}}' + + # discriminator mappings + __discriminator_value_class_map: ClassVar[Dict[str, str]] = { + {{#mappedModels}}'{{{mappingName}}}': '{{{modelName}}}'{{^-last}},{{/-last}}{{/mappedModels}} + } + + @classmethod + def get_discriminator_value(cls, obj: Dict[str, Any]) -> Optional[str]: + """Returns the discriminator value (object type) of the data""" + discriminator_value = obj[cls.__discriminator_property_name] + if discriminator_value: + return cls.__discriminator_value_class_map.get(discriminator_value) + else: + return None + +{{/discriminator}} +{{/hasChildren}} + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[{{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#mappedModels}}{{{modelName}}}{{^-last}}, {{/-last}}{{/mappedModels}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}]: + """Create an instance of {{{classname}}} from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + {{#vendorExtensions.x-py-readonly}} + * OpenAPI `readOnly` fields are excluded. + {{/vendorExtensions.x-py-readonly}} + {{#isAdditionalPropertiesTrue}} + * Fields in `self.additional_properties` are added to the output dict. + {{/isAdditionalPropertiesTrue}} + """ + excluded_fields: Set[str] = set([ + {{#vendorExtensions.x-py-readonly}} + "{{{.}}}", + {{/vendorExtensions.x-py-readonly}} + {{#isAdditionalPropertiesTrue}} + "additional_properties", + {{/isAdditionalPropertiesTrue}} + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + {{#allVars}} + {{#isContainer}} + {{#isArray}} + {{#items.isArray}} + {{^items.items.isPrimitiveType}} + # override the default output from pydantic by calling `to_dict()` of each item in {{{name}}} (list of list) + _items = [] + if self.{{{name}}}: + for _item_{{{name}}} in self.{{{name}}}: + if _item_{{{name}}}: + _items.append( + [_inner_item.to_dict() for _inner_item in _item_{{{name}}} if _inner_item is not None] + ) + _dict['{{{baseName}}}'] = _items + {{/items.items.isPrimitiveType}} + {{/items.isArray}} + {{^items.isArray}} + {{^items.isPrimitiveType}} + {{^items.isEnumOrRef}} + # override the default output from pydantic by calling `to_dict()` of each item in {{{name}}} (list) + _items = [] + if self.{{{name}}}: + for _item_{{{name}}} in self.{{{name}}}: + if _item_{{{name}}}: + _items.append(_item_{{{name}}}.to_dict()) + _dict['{{{baseName}}}'] = _items + {{/items.isEnumOrRef}} + {{/items.isPrimitiveType}} + {{/items.isArray}} + {{/isArray}} + {{#isMap}} + {{#items.isArray}} + {{^items.items.isPrimitiveType}} + # override the default output from pydantic by calling `to_dict()` of each value in {{{name}}} (dict of array) + _field_dict_of_array = {} + if self.{{{name}}}: + for _key_{{{name}}} in self.{{{name}}}: + if self.{{{name}}}[_key_{{{name}}}] is not None: + _field_dict_of_array[_key_{{{name}}}] = [ + _item.to_dict() for _item in self.{{{name}}}[_key_{{{name}}}] + ] + _dict['{{{baseName}}}'] = _field_dict_of_array + {{/items.items.isPrimitiveType}} + {{/items.isArray}} + {{^items.isArray}} + {{^items.isPrimitiveType}} + {{^items.isEnumOrRef}} + # override the default output from pydantic by calling `to_dict()` of each value in {{{name}}} (dict) + _field_dict = {} + if self.{{{name}}}: + for _key_{{{name}}} in self.{{{name}}}: + if self.{{{name}}}[_key_{{{name}}}]: + _field_dict[_key_{{{name}}}] = self.{{{name}}}[_key_{{{name}}}].to_dict() + _dict['{{{baseName}}}'] = _field_dict + {{/items.isEnumOrRef}} + {{/items.isPrimitiveType}} + {{/items.isArray}} + {{/isMap}} + {{/isContainer}} + {{^isContainer}} + {{^isPrimitiveType}} + {{^isEnumOrRef}} + # override the default output from pydantic by calling `to_dict()` of {{{name}}} + if self.{{{name}}}: + _dict['{{{baseName}}}'] = self.{{{name}}}.to_dict() + {{/isEnumOrRef}} + {{/isPrimitiveType}} + {{/isContainer}} + {{/allVars}} + {{#isAdditionalPropertiesTrue}} + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + {{/isAdditionalPropertiesTrue}} + {{#allVars}} + {{#isNullable}} + # set to None if {{{name}}} (nullable) is None + # and model_fields_set contains the field + if self.{{name}} is None and "{{{name}}}" in self.model_fields_set: + _dict['{{{baseName}}}'] = None + + {{/isNullable}} + {{/allVars}} + return _dict + + {{#hasChildren}} + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Optional[{{#discriminator}}Union[{{#mappedModels}}{{{modelName}}}{{^-last}}, {{/-last}}{{/mappedModels}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}]: + """Create an instance of {{{classname}}} from a dict""" + {{#discriminator}} + # look up the object type based on discriminator mapping + object_type = cls.get_discriminator_value(obj) + {{#mappedModels}} + if object_type == '{{{modelName}}}': + return import_module("{{packageName}}.models.{{model.classFilename}}").{{modelName}}.from_dict(obj) + {{/mappedModels}} + + raise ValueError("{{{classname}}} failed to lookup discriminator value from " + + json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + + ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) + {{/discriminator}} + {{/hasChildren}} + {{^hasChildren}} + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of {{{classname}}} from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + {{#disallowAdditionalPropertiesIfNotPresent}} + {{^isAdditionalPropertiesTrue}} + # raise errors for additional fields in the input + for _key in obj.keys(): + if _key not in cls.__properties: + raise ValueError("Error due to additional fields (not defined in {{classname}}) in the input: " + _key) + + {{/isAdditionalPropertiesTrue}} + {{/disallowAdditionalPropertiesIfNotPresent}} + _obj = cls.model_validate({ + {{#allVars}} + {{#isContainer}} + {{#isArray}} + {{#items.isArray}} + {{#items.items.isPrimitiveType}} + "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} + {{/items.items.isPrimitiveType}} + {{^items.items.isPrimitiveType}} + "{{{baseName}}}": [ + [{{{items.items.dataType}}}.from_dict(_inner_item) for _inner_item in _item] + for _item in obj["{{{baseName}}}"] + ] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} + {{/items.items.isPrimitiveType}} + {{/items.isArray}} + {{^items.isArray}} + {{^items.isPrimitiveType}} + {{#items.isEnumOrRef}} + "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} + {{/items.isEnumOrRef}} + {{^items.isEnumOrRef}} + "{{{baseName}}}": [{{{items.dataType}}}.from_dict(_item) for _item in obj["{{{baseName}}}"]] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} + {{/items.isEnumOrRef}} + {{/items.isPrimitiveType}} + {{#items.isPrimitiveType}} + "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} + {{/items.isPrimitiveType}} + {{/items.isArray}} + {{/isArray}} + {{#isMap}} + {{^items.isPrimitiveType}} + {{^items.isEnumOrRef}} + {{#items.isContainer}} + {{#items.isMap}} + "{{{baseName}}}": dict( + (_k, dict( + (_ik, {{{items.items.dataType}}}.from_dict(_iv)) + for _ik, _iv in _v.items() + ) + if _v is not None + else None + ) + for _k, _v in obj.get("{{{baseName}}}").items() + ) + if obj.get("{{{baseName}}}") is not None + else None{{^-last}},{{/-last}} + {{/items.isMap}} + {{#items.isArray}} + "{{{baseName}}}": dict( + (_k, + [{{{items.items.dataType}}}.from_dict(_item) for _item in _v] + if _v is not None + else None + ) + for _k, _v in obj.get("{{{baseName}}}", {}).items() + ){{^-last}},{{/-last}} + {{/items.isArray}} + {{/items.isContainer}} + {{^items.isContainer}} + "{{{baseName}}}": dict( + (_k, {{{items.dataType}}}.from_dict(_v)) + for _k, _v in obj["{{{baseName}}}"].items() + ) + if obj.get("{{{baseName}}}") is not None + else None{{^-last}},{{/-last}} + {{/items.isContainer}} + {{/items.isEnumOrRef}} + {{#items.isEnumOrRef}} + "{{{baseName}}}": dict((_k, _v) for _k, _v in obj.get("{{{baseName}}}").items()) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} + {{/items.isEnumOrRef}} + {{/items.isPrimitiveType}} + {{#items.isPrimitiveType}} + "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} + {{/items.isPrimitiveType}} + {{/isMap}} + {{/isContainer}} + {{^isContainer}} + {{^isPrimitiveType}} + {{^isEnumOrRef}} + "{{{baseName}}}": {{{dataType}}}.from_dict(obj["{{{baseName}}}"]) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} + {{/isEnumOrRef}} + {{#isEnumOrRef}} + "{{{baseName}}}": obj.get("{{{baseName}}}"){{#defaultValue}} if obj.get("{{baseName}}") is not None else {{defaultValue}}{{/defaultValue}}{{^-last}},{{/-last}} + {{/isEnumOrRef}} + {{/isPrimitiveType}} + {{#isPrimitiveType}} + {{#defaultValue}} + "{{{baseName}}}": obj.get("{{{baseName}}}") if obj.get("{{{baseName}}}") is not None else {{{defaultValue}}}{{^-last}},{{/-last}} + {{/defaultValue}} + {{^defaultValue}} + "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} + {{/defaultValue}} + {{/isPrimitiveType}} + {{/isContainer}} + {{/allVars}} + }) + {{#isAdditionalPropertiesTrue}} + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + {{/isAdditionalPropertiesTrue}} + return _obj + {{/hasChildren}} + +{{#vendorExtensions.x-py-postponed-model-imports.size}} +{{#vendorExtensions.x-py-postponed-model-imports}} +{{{.}}} +{{/vendorExtensions.x-py-postponed-model-imports}} +# TODO: Rewrite to not use raise_errors +{{classname}}.model_rebuild(raise_errors=False) +{{/vendorExtensions.x-py-postponed-model-imports.size}}