-
-
Notifications
You must be signed in to change notification settings - Fork 7.4k
enhance python openapi generator #22600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
cbb6665
dec179a
bf5eff7
ddad4aa
5fd3663
81c2226
bc25f93
039a1bd
f1070a5
5da74e6
86221f8
e5b2cb5
279d739
0ab86bf
7030a2e
d4cc4f6
3583dd7
2f6e52f
9a41599
ed84fb5
6189c8a
ad72128
370515e
1a1736b
867f497
918a028
8a7c0af
82bb97c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -321,7 +321,7 @@ class ApiClient: | |||||||||||||||||||
| return_data = self.__deserialize_file(response_data) | ||||||||||||||||||||
| elif response_type is not None: | ||||||||||||||||||||
| match = None | ||||||||||||||||||||
| content_type = response_data.headers.get('content-type') | ||||||||||||||||||||
| content_type = response_data.getheader('content-type') | ||||||||||||||||||||
| if content_type is not None: | ||||||||||||||||||||
| match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) | ||||||||||||||||||||
| encoding = match.group(1) if match else "utf-8" | ||||||||||||||||||||
|
|
@@ -338,7 +338,7 @@ class ApiClient: | |||||||||||||||||||
| return ApiResponse( | ||||||||||||||||||||
| status_code = response_data.status, | ||||||||||||||||||||
| data = return_data, | ||||||||||||||||||||
| headers = response_data.headers, | ||||||||||||||||||||
| headers = response_data.getheaders(), | ||||||||||||||||||||
| raw_data = response_data.data | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -389,13 +389,10 @@ class ApiClient: | |||||||||||||||||||
| # and attributes which value is not None. | ||||||||||||||||||||
| # Convert attribute name to json key in | ||||||||||||||||||||
| # model definition for request. | ||||||||||||||||||||
| if hasattr(obj, 'to_dict') and callable(getattr(obj, 'to_dict')): | ||||||||||||||||||||
| obj_dict = obj.to_dict() | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| obj_dict = obj.__dict__ | ||||||||||||||||||||
| obj_dict = obj.model_dump(by_alias=True, exclude_none=True) | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: RootModel values now cause sanitize_for_serialization to crash because model_dump may return a non-dict, leading to .items() AttributeError Prompt for AI agents
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Serialization now always drops Prompt for AI agents
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Hardcoding Prompt for AI agents
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| if isinstance(obj_dict, list): | ||||||||||||||||||||
| # here we handle instances that can either be a list or something else, and only became a real list by calling to_dict() | ||||||||||||||||||||
| # here we handle instances that can either be a list or something else, and only became a real list by calling model_dump(by_alias=True) | ||||||||||||||||||||
| return self.sanitize_for_serialization(obj_dict) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return { | ||||||||||||||||||||
|
|
@@ -719,14 +716,14 @@ class ApiClient: | |||||||||||||||||||
| os.close(fd) | ||||||||||||||||||||
| os.remove(path) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| content_disposition = response.headers.get("Content-Disposition") | ||||||||||||||||||||
| content_disposition = response.getheader("Content-Disposition") | ||||||||||||||||||||
| if content_disposition: | ||||||||||||||||||||
| m = re.search( | ||||||||||||||||||||
| r'filename=[\'"]?([^\'"\s]+)[\'"]?', | ||||||||||||||||||||
cubic-dev-ai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
| content_disposition | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| assert m is not None, "Unexpected 'content-disposition' header value" | ||||||||||||||||||||
| filename = m.group(1) | ||||||||||||||||||||
| filename = os.path.basename(m.group(1)) # Strip any directory traversal | ||||||||||||||||||||
| path = os.path.join(os.path.dirname(path), filename) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| with open(path, "wb") as f: | ||||||||||||||||||||
|
|
@@ -819,4 +816,4 @@ class ApiClient: | |||||||||||||||||||
| :return: model object. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return klass.from_dict(data) | ||||||||||||||||||||
| return klass.model_validate(data) | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,8 +14,7 @@ from logging import FileHandler | |
| import multiprocessing | ||
| {{/async}} | ||
| import sys | ||
| from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union | ||
| from typing_extensions import NotRequired, Self | ||
| from typing import Any, ClassVar, Dict, List, Literal, NotRequired, Optional, TypedDict, Union, Self | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Importing NotRequired/Self directly from typing breaks Python <3.11 because these names are unavailable there; the template lacks a fallback or enforced 3.11+ requirement. Prompt for AI agents |
||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. breaking change for python 3.9 python -c "from typing import Self; print('ok')"
Traceback (most recent call last):
File "<string>", line 1, in <module>
ImportError: cannot import name 'Self' from 'typing' |
||
| {{^async}} | ||
| import urllib3 | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,6 @@ | ||||||||||||||
| # coding: utf-8 | ||||||||||||||
|
|
||||||||||||||
| {{>partial_header}} | ||||||||||||||
|
|
||||||||||||||
| from typing import Any, Optional | ||||||||||||||
| from typing_extensions import Self | ||||||||||||||
| from typing import Any, Optional, Self | ||||||||||||||
|
|
||||||||||||||
| class OpenApiException(Exception): | ||||||||||||||
| """The base exception class for all OpenAPIExceptions""" | ||||||||||||||
|
|
@@ -95,9 +92,9 @@ class ApiKeyError(OpenApiException, KeyError): | |||||||||||||
| class ApiException(OpenApiException): | ||||||||||||||
|
|
||||||||||||||
| def __init__( | ||||||||||||||
| self, | ||||||||||||||
| status=None, | ||||||||||||||
| reason=None, | ||||||||||||||
| self, | ||||||||||||||
| status=None, | ||||||||||||||
| reason=None, | ||||||||||||||
| http_resp=None, | ||||||||||||||
| *, | ||||||||||||||
| body: Optional[str] = None, | ||||||||||||||
|
|
@@ -119,14 +116,14 @@ class ApiException(OpenApiException): | |||||||||||||
| self.body = http_resp.data.decode('utf-8') | ||||||||||||||
| except Exception: | ||||||||||||||
| pass | ||||||||||||||
| self.headers = http_resp.headers | ||||||||||||||
| self.headers = http_resp.getheaders() | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Using http_resp.getheaders() breaks compatibility with urllib3 v2 where getheaders() was removed; use the headers attribute instead to avoid AttributeError. Prompt for AI agents
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| @classmethod | ||||||||||||||
| def from_response( | ||||||||||||||
| cls, | ||||||||||||||
| *, | ||||||||||||||
| http_resp, | ||||||||||||||
| body: Optional[str], | ||||||||||||||
| cls, | ||||||||||||||
| *, | ||||||||||||||
| http_resp, | ||||||||||||||
| body: Optional[str], | ||||||||||||||
| data: Optional[Any], | ||||||||||||||
| ) -> Self: | ||||||||||||||
| if http_resp.status == 400: | ||||||||||||||
|
|
@@ -160,11 +157,8 @@ class ApiException(OpenApiException): | |||||||||||||
| error_message += "HTTP response headers: {0}\n".format( | ||||||||||||||
| self.headers) | ||||||||||||||
|
|
||||||||||||||
| if self.body: | ||||||||||||||
| error_message += "HTTP response body: {0}\n".format(self.body) | ||||||||||||||
|
|
||||||||||||||
| if self.data: | ||||||||||||||
| error_message += "HTTP response data: {0}\n".format(self.data) | ||||||||||||||
| if self.data or self.body: | ||||||||||||||
| error_message += "HTTP response body: {0}\n".format(self.data or self.body) | ||||||||||||||
|
Comment on lines
+160
to
+161
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Using if self.data is not None or self.body:
error_message += "HTTP response body: {0}\n".format(self.data if self.data is not None else self.body)Prompt for AI agents
Suggested change
|
||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+161
to
162
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: ApiException str now drops one of body/data and mislabels data as body due to Prompt for AI agents
Suggested change
|
||||||||||||||
| return error_message | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,177 +1,59 @@ | ||||||||||
| from __future__ import annotations | ||||||||||
| from inspect import getfullargspec | ||||||||||
| import json | ||||||||||
| import pprint | ||||||||||
| import re # noqa: F401 | ||||||||||
| {{#vendorExtensions.x-py-other-imports}} | ||||||||||
| {{{.}}} | ||||||||||
| {{/vendorExtensions.x-py-other-imports}} | ||||||||||
| {{#vendorExtensions.x-py-model-imports}} | ||||||||||
| {{{.}}} | ||||||||||
| {{/vendorExtensions.x-py-model-imports}} | ||||||||||
| from typing import Union, Any, List, Set, TYPE_CHECKING, Optional, Dict | ||||||||||
| from typing_extensions import Literal, Self | ||||||||||
| from pydantic import Field | ||||||||||
| from pydantic import Field, RootModel | ||||||||||
| from typing import Any, Dict, List, Union, Self | ||||||||||
|
|
||||||||||
| {{#lambda.uppercase}}{{{classname}}}{{/lambda.uppercase}}_ANY_OF_SCHEMAS = [{{#anyOf}}"{{.}}"{{^-last}}, {{/-last}}{{/anyOf}}] | ||||||||||
|
|
||||||||||
| class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}): | ||||||||||
|
|
||||||||||
| class {{classname}}(RootModel[Union[{{#anyOf}}{{.}}{{^-last}}, {{/-last}}{{/anyOf}}]]): | ||||||||||
cubic-dev-ai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: RootModel base evaluates anyOf types before postponed imports, risking NameError when any union member is only imported in the deferred block. Prompt for AI agents |
||||||||||
| """ | ||||||||||
| {{{description}}}{{^description}}{{{classname}}}{{/description}} | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| {{#composedSchemas.anyOf}} | ||||||||||
| # data type: {{{dataType}}} | ||||||||||
| {{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}} | ||||||||||
| {{/composedSchemas.anyOf}} | ||||||||||
| if TYPE_CHECKING: | ||||||||||
| actual_instance: Optional[Union[{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}]] = None | ||||||||||
| else: | ||||||||||
| actual_instance: Any = None | ||||||||||
| any_of_schemas: Set[str] = { {{#anyOf}}"{{.}}"{{^-last}}, {{/-last}}{{/anyOf}} } | ||||||||||
|
|
||||||||||
| model_config = { | ||||||||||
| "validate_assignment": True, | ||||||||||
| "protected_namespaces": (), | ||||||||||
| } | ||||||||||
| {{#discriminator}} | ||||||||||
|
|
||||||||||
| discriminator_value_class_map: Dict[str, str] = { | ||||||||||
| {{#children}} | ||||||||||
| '{{^vendorExtensions.x-discriminator-value}}{{name}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}': '{{{classname}}}'{{^-last}},{{/-last}} | ||||||||||
| {{/children}} | ||||||||||
| } | ||||||||||
| {{/discriminator}} | ||||||||||
| root: Union[{{#anyOf}}{{.}}{{^-last}}, {{/-last}}{{/anyOf}}{{#isNullable}}, None{{/isNullable}}] = Field( | ||||||||||
| {{#isNullable}}None{{/isNullable}}{{^isNullable}}...{{/isNullable}}{{#discriminator}}, discriminator="{{discriminatorName}}"{{/discriminator}} | ||||||||||
| ) | ||||||||||
cubic-dev-ai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
|
||||||||||
| def __init__(self, *args, **kwargs) -> None: | ||||||||||
| if args: | ||||||||||
| if len(args) > 1: | ||||||||||
| raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") | ||||||||||
| if kwargs: | ||||||||||
| raise ValueError("If a position argument is used, keyword arguments cannot be used.") | ||||||||||
| super().__init__(actual_instance=args[0]) | ||||||||||
| else: | ||||||||||
| super().__init__(**kwargs) | ||||||||||
| def __getattr__(self, name): | ||||||||||
| """ | ||||||||||
| Delegate attribute access to the root model if the attribute | ||||||||||
| doesn't exist on the main class. | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| @field_validator('actual_instance') | ||||||||||
| def actual_instance_must_validate_anyof(cls, v): | ||||||||||
| {{#isNullable}} | ||||||||||
| if v is None: | ||||||||||
| return v | ||||||||||
| if name in self.__dict__: | ||||||||||
| return super().__getattribute__(name) | ||||||||||
|
|
||||||||||
| {{/isNullable}} | ||||||||||
| instance = {{{classname}}}.model_construct() | ||||||||||
| error_messages = [] | ||||||||||
| {{#composedSchemas.anyOf}} | ||||||||||
| # validate data type: {{{dataType}}} | ||||||||||
| {{#isContainer}} | ||||||||||
| try: | ||||||||||
| instance.{{vendorExtensions.x-py-name}} = v | ||||||||||
| return v | ||||||||||
| except (ValidationError, ValueError) as e: | ||||||||||
| error_messages.append(str(e)) | ||||||||||
| {{/isContainer}} | ||||||||||
| {{^isContainer}} | ||||||||||
| {{#isPrimitiveType}} | ||||||||||
| try: | ||||||||||
| instance.{{vendorExtensions.x-py-name}} = v | ||||||||||
| return v | ||||||||||
| except (ValidationError, ValueError) as e: | ||||||||||
| error_messages.append(str(e)) | ||||||||||
| {{/isPrimitiveType}} | ||||||||||
| {{^isPrimitiveType}} | ||||||||||
| if not isinstance(v, {{{dataType}}}): | ||||||||||
| error_messages.append(f"Error! Input type `{type(v)}` is not `{{{dataType}}}`") | ||||||||||
| else: | ||||||||||
| return v | ||||||||||
| root = self.__dict__.get('root') | ||||||||||
| if root is not None: | ||||||||||
| return getattr(root, name) | ||||||||||
|
|
||||||||||
| {{/isPrimitiveType}} | ||||||||||
| {{/isContainer}} | ||||||||||
| {{/composedSchemas.anyOf}} | ||||||||||
| if error_messages: | ||||||||||
| # no match | ||||||||||
| raise ValueError("No match found when setting the actual_instance in {{{classname}}} with anyOf schemas: {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}. Details: " + ", ".join(error_messages)) | ||||||||||
| else: | ||||||||||
| return v | ||||||||||
| raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") | ||||||||||
|
|
||||||||||
| @classmethod | ||||||||||
| def from_dict(cls, obj: Dict[str, Any]) -> Self: | ||||||||||
| return cls.from_json(json.dumps(obj)) | ||||||||||
| """Returns the object represented by the python Dict""" | ||||||||||
| return cls.model_validate(obj, strict=True) | ||||||||||
|
|
||||||||||
| @classmethod | ||||||||||
| def from_json(cls, json_str: str) -> Self: | ||||||||||
| """Returns the object represented by the json string""" | ||||||||||
| instance = cls.model_construct() | ||||||||||
| {{#isNullable}} | ||||||||||
| if json_str is None: | ||||||||||
| return instance | ||||||||||
|
|
||||||||||
| {{/isNullable}} | ||||||||||
| error_messages = [] | ||||||||||
| {{#composedSchemas.anyOf}} | ||||||||||
| {{#isContainer}} | ||||||||||
| # deserialize data into {{{dataType}}} | ||||||||||
| try: | ||||||||||
| # validation | ||||||||||
| instance.{{vendorExtensions.x-py-name}} = json.loads(json_str) | ||||||||||
| # assign value to actual_instance | ||||||||||
| instance.actual_instance = instance.{{vendorExtensions.x-py-name}} | ||||||||||
| return instance | ||||||||||
| except (ValidationError, ValueError) as e: | ||||||||||
| error_messages.append(str(e)) | ||||||||||
| {{/isContainer}} | ||||||||||
| {{^isContainer}} | ||||||||||
| {{#isPrimitiveType}} | ||||||||||
| # deserialize data into {{{dataType}}} | ||||||||||
| try: | ||||||||||
| # validation | ||||||||||
| instance.{{vendorExtensions.x-py-name}} = json.loads(json_str) | ||||||||||
| # assign value to actual_instance | ||||||||||
| instance.actual_instance = instance.{{vendorExtensions.x-py-name}} | ||||||||||
| return instance | ||||||||||
| except (ValidationError, ValueError) as e: | ||||||||||
| error_messages.append(str(e)) | ||||||||||
| {{/isPrimitiveType}} | ||||||||||
| {{^isPrimitiveType}} | ||||||||||
| # {{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}} | ||||||||||
| try: | ||||||||||
| instance.actual_instance = {{{dataType}}}.from_json(json_str) | ||||||||||
| return instance | ||||||||||
| except (ValidationError, ValueError) as e: | ||||||||||
| error_messages.append(str(e)) | ||||||||||
| {{/isPrimitiveType}} | ||||||||||
| {{/isContainer}} | ||||||||||
| {{/composedSchemas.anyOf}} | ||||||||||
|
|
||||||||||
| if error_messages: | ||||||||||
| # no match | ||||||||||
| raise ValueError("No match found when deserializing the JSON string into {{{classname}}} with anyOf schemas: {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}. Details: " + ", ".join(error_messages)) | ||||||||||
| else: | ||||||||||
| return instance | ||||||||||
| return cls.model_validate_json(json_str) | ||||||||||
|
|
||||||||||
| def to_json(self) -> str: | ||||||||||
| """Returns the JSON representation of the actual instance""" | ||||||||||
| if self.actual_instance is None: | ||||||||||
| return "null" | ||||||||||
| return self.model_dump_json(by_alias=True) | ||||||||||
|
|
||||||||||
| if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): | ||||||||||
| return self.actual_instance.to_json() | ||||||||||
| else: | ||||||||||
| return json.dumps(self.actual_instance) | ||||||||||
|
|
||||||||||
| def to_dict(self) -> Optional[Union[Dict[str, Any], {{#anyOf}}{{.}}{{^-last}}, {{/-last}}{{/anyOf}}]]: | ||||||||||
| def to_dict(self) -> Dict[str, Any]: | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: to_dict is typed as Dict[str, Any], but RootModel.model_dump returns the root value directly (can be primitive or list for anyOf). This misleads type checkers and callers when the root is not a dict. Prompt for AI agents
Suggested change
|
||||||||||
| """Returns the dict representation of the actual instance""" | ||||||||||
| if self.actual_instance is None: | ||||||||||
| return None | ||||||||||
|
|
||||||||||
| if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): | ||||||||||
| return self.actual_instance.to_dict() | ||||||||||
| else: | ||||||||||
| return self.actual_instance | ||||||||||
| return self.model_dump(by_alias=True) | ||||||||||
|
|
||||||||||
| def to_str(self) -> str: | ||||||||||
| """Returns the string representation of the actual instance""" | ||||||||||
| return pprint.pformat(self.model_dump()) | ||||||||||
| return pprint.pformat(self.model_dump(by_alias=True, mode="json")) | ||||||||||
|
|
||||||||||
| {{#vendorExtensions.x-py-postponed-model-imports.size}} | ||||||||||
| {{#vendorExtensions.x-py-postponed-model-imports}} | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
breaking change 👀