From 8f24214a57ea5c03867cec7f661dc65f4022ca18 Mon Sep 17 00:00:00 2001 From: Prawal Gangwar Date: Fri, 23 Dec 2022 23:13:52 +0530 Subject: [PATCH 1/4] warpper class to contain all api calls --- openapi_python_client/__init__.py | 23 ++++++- openapi_python_client/parser/openapi.py | 9 ++- .../templates/endpoint_macros.py.jinja | 69 +++++++++++++++++++ .../templates/wrapper.py.jinja | 31 +++++++++ 4 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 openapi_python_client/templates/wrapper.py.jinja diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index ff0a132ca..46d959078 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -123,6 +123,7 @@ def build(self) -> Sequence[GeneratorError]: self._create_package() self._build_metadata() self._build_models() + self._build_wrapper() self._build_api() self._run_post_hooks() return self._get_errors() @@ -255,7 +256,6 @@ def _build_models(self) -> None: module_path.write_text(str_enum_template.render(enum=enum), encoding=self.file_encoding) imports.append(import_string_from_class(enum.class_info)) alls.append(enum.class_info.name) - models_init_template = self.env.get_template("models_init.py.jinja") models_init.write_text(models_init_template.render(imports=imports, alls=alls), encoding=self.file_encoding) @@ -302,6 +302,27 @@ def _build_api(self) -> None: encoding=self.file_encoding, ) + # pylint: disable=too-many-locals + def _build_wrapper(self) -> None: + # Generate Client Wrapper + wrapper_path = self.package_dir / "sail_class.py" + wrapper_template = self.env.get_template("wrapper.py.jinja") + + imports = [] + for model in self.openapi.models: + imports.append(import_string_from_class(model.class_info, prefix="models")) + + for enum in self.openapi.enums: + imports.append(import_string_from_class(enum.class_info, prefix="models")) + + endpoint_collections_by_tag = self.openapi.endpoint_collections_by_tag + for tag, collection in endpoint_collections_by_tag.items(): + for endpoint in collection.endpoints: + print("build", endpoint.name) + wrapper_path.write_text( + wrapper_template.render(imports=imports, endpoints=collection.endpoints), encoding=self.file_encoding + ) + def _get_project_for_url_or_path( # pylint: disable=too-many-arguments url: Optional[str], diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index b7c4a8142..c2c2a8945 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -544,10 +544,10 @@ class GeneratorData: title: str description: Optional[str] version: str - models: Iterator[ModelProperty] + models: List[ModelProperty] errors: List[ParseError] endpoint_collections_by_tag: Dict[utils.PythonIdentifier, EndpointCollection] - enums: Iterator[EnumProperty] + enums: List[EnumProperty] @staticmethod def from_dict(data: Dict[str, Any], *, config: Config) -> Union["GeneratorData", GeneratorError]: @@ -571,9 +571,8 @@ def from_dict(data: Dict[str, Any], *, config: Config) -> Union["GeneratorData", data=openapi.paths, schemas=schemas, parameters=parameters, config=config ) - enums = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, EnumProperty)) - models = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, ModelProperty)) - + enums = [prop for prop in schemas.classes_by_name.values() if isinstance(prop, EnumProperty)] + models = [prop for prop in schemas.classes_by_name.values() if isinstance(prop, ModelProperty)] return GeneratorData( title=openapi.info.title, description=openapi.info.description, diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 4dc0575f9..149d8e118 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -167,3 +167,72 @@ Returns: Response[{{ return_string }}] """ {% endmacro %} + + +{% macro arguments_passing(endpoint) %} +{# path parameters #} +{% for parameter in endpoint.path_parameters.values() %} +{{ parameter.python_name }} = {{ parameter.python_name }}, +{% endfor %} +{# Proper client based on whether or not the endpoint requires authentication #} +{% if endpoint.requires_security %} +client = self._client, +{% else %} +client = self._client, +{% endif %} +{# Form data if any #} +{% if endpoint.form_body %} +form_data = form_data, +{% endif %} +{# Multipart data if any #} +{% if endpoint.multipart_body %} +multipart_data = multipart_data, +{% endif %} +{# JSON body if any #} +{% if endpoint.json_body %} +json_body = json_body, +{% endif %} +{# query parameters #} +{% for parameter in endpoint.query_parameters.values() %} +{{ parameter.python_name }} = {{ parameter.python_name }}, +{% endfor %} +{% for parameter in endpoint.header_parameters.values() %} +{{ parameter.python_name }} = {{ parameter.python_name }}, +{% endfor %} +{# cookie parameters #} +{% for parameter in endpoint.cookie_parameters.values() %} +{{ parameter.python_name }} = {{ parameter.python_name }}, +{% endfor %} +{% endmacro %} + + +{# The all the kwargs passed into an endpoint (and variants thereof)) #} +{% macro arguments_no_client(endpoint) %} +{# path parameters #} +{% for parameter in endpoint.path_parameters.values() %} +{{ parameter.to_string() }}, +{% endfor %} +{# Form data if any #} +{% if endpoint.form_body %} +form_data: {{ endpoint.form_body.get_type_string() }}, +{% endif %} +{# Multipart data if any #} +{% if endpoint.multipart_body %} +multipart_data: {{ endpoint.multipart_body.get_type_string() }}, +{% endif %} +{# JSON body if any #} +{% if endpoint.json_body %} +json_body: {{ endpoint.json_body.get_type_string() }}, +{% endif %} +{# query parameters #} +{% for parameter in endpoint.query_parameters.values() %} +{{ parameter.to_string() }}, +{% endfor %} +{% for parameter in endpoint.header_parameters.values() %} +{{ parameter.to_string() }}, +{% endfor %} +{# cookie parameters #} +{% for parameter in endpoint.cookie_parameters.values() %} +{{ parameter.to_string() }}, +{% endfor %} +{% endmacro %} diff --git a/openapi_python_client/templates/wrapper.py.jinja b/openapi_python_client/templates/wrapper.py.jinja new file mode 100644 index 000000000..6236032d4 --- /dev/null +++ b/openapi_python_client/templates/wrapper.py.jinja @@ -0,0 +1,31 @@ +from client import Client, AuthenticatedClient +from typing import Union, Any +from http import HTTPStatus +from .types import UNSET, Unset + +{% for import in imports | sort %} +{{ import }} +{% endfor %} + +{% for endpoint in endpoints %} +from api.default import {{ endpoint.name }} +{% endfor %} + +{% from "endpoint_macros.py.jinja" import header_params, cookie_params, query_params, json_body, multipart_body, + arguments, arguments_passing, arguments_no_client, client, kwargs, parse_response, docstring %} + +class SyncApis: + def __init__(self, client: AuthenticatedClient) -> None: + self._client = client + + {% for endpoint in endpoints %} + def {{ endpoint.name }}(self, {{ arguments_no_client(endpoint) | indent(4)}}) -> {{ endpoint.response_type() }}: + response = {{ endpoint.name }}.sync( + {{ arguments_passing(endpoint) | indent(8)}} + ) + if response is None: + raise Exception("No response") + + return response + + {% endfor %} From 43eb3b30ab26f4998c2f62b4807d634f93a23d1c Mon Sep 17 00:00:00 2001 From: Prawal Gangwar Date: Sat, 24 Dec 2022 01:15:17 +0530 Subject: [PATCH 2/4] throw exception on fail response --- openapi_python_client/__init__.py | 3 --- openapi_python_client/templates/endpoint_module.py.jinja | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 46d959078..9ff39ddfa 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -14,7 +14,6 @@ import httpx import yaml from jinja2 import BaseLoader, ChoiceLoader, Environment, FileSystemLoader, PackageLoader - from openapi_python_client import utils from .config import Config @@ -317,8 +316,6 @@ def _build_wrapper(self) -> None: endpoint_collections_by_tag = self.openapi.endpoint_collections_by_tag for tag, collection in endpoint_collections_by_tag.items(): - for endpoint in collection.endpoints: - print("build", endpoint.name) wrapper_path.write_text( wrapper_template.render(imports=imports, endpoints=collection.endpoints), encoding=self.file_encoding ) diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 26d313f16..c73571ac0 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -60,6 +60,10 @@ def _get_kwargs( def _parse_response(*, client: Client, response: httpx.Response) -> Optional[{{ return_string }}]: + + if response.status_code < 200 or response.status_code >= 300: + raise Exception(f"Failure status code: {response.status_code}. Details: {response.text}") + {% for response in endpoint.responses %} if response.status_code == HTTPStatus.{{ response.status_code.name }}: {% if parsed_responses %}{% import "property_templates/" + response.prop.template as prop_template %} From bb2bce217df9f0c6dcc5f165f704d422096436e4 Mon Sep 17 00:00:00 2001 From: Prawal Gangwar Date: Tue, 27 Dec 2022 00:56:14 +0530 Subject: [PATCH 3/4] separate auth and unauth class --- openapi_python_client/__init__.py | 4 +-- openapi_python_client/parser/openapi.py | 16 ++++++++++ .../templates/wrapper.py.jinja | 31 ++++++++++++++++--- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 9ff39ddfa..8bb536440 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -309,10 +309,10 @@ def _build_wrapper(self) -> None: imports = [] for model in self.openapi.models: - imports.append(import_string_from_class(model.class_info, prefix="models")) + imports.append(import_string_from_class(model.class_info, prefix=".models")) for enum in self.openapi.enums: - imports.append(import_string_from_class(enum.class_info, prefix="models")) + imports.append(import_string_from_class(enum.class_info, prefix=".models")) endpoint_collections_by_tag = self.openapi.endpoint_collections_by_tag for tag, collection in endpoint_collections_by_tag.items(): diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index c2c2a8945..b55ab2df4 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -512,6 +512,22 @@ def from_data( return result, schemas, parameters + def success_response_type(self) -> str: + """Get the Python type of success response from this endpoint""" + types = sorted({response.prop.get_type_string(quoted=False) for response in self.responses}) + if len(types) == 0: + return "Any" + if len(types) == 1: + return self.responses[0].prop.get_type_string(quoted=False) + + # If there is a 2xx response, use that + for response in self.responses: + if response.status_code >= 200 and response.status_code < 300: + return response.prop.get_type_string(quoted=False) + + # Otherwise, use the Union of all responses + return f"Union[{', '.join(types)}]" + def response_type(self) -> str: """Get the Python type of any response from this endpoint""" types = sorted({response.prop.get_type_string(quoted=False) for response in self.responses}) diff --git a/openapi_python_client/templates/wrapper.py.jinja b/openapi_python_client/templates/wrapper.py.jinja index 6236032d4..bbc8e6685 100644 --- a/openapi_python_client/templates/wrapper.py.jinja +++ b/openapi_python_client/templates/wrapper.py.jinja @@ -1,4 +1,4 @@ -from client import Client, AuthenticatedClient +from .client import Client, AuthenticatedClient from typing import Union, Any from http import HTTPStatus from .types import UNSET, Unset @@ -8,24 +8,47 @@ from .types import UNSET, Unset {% endfor %} {% for endpoint in endpoints %} -from api.default import {{ endpoint.name }} +from .api.default import {{ endpoint.name }} {% endfor %} {% from "endpoint_macros.py.jinja" import header_params, cookie_params, query_params, json_body, multipart_body, arguments, arguments_passing, arguments_no_client, client, kwargs, parse_response, docstring %} -class SyncApis: +class SyncAuthenticatedOperations: def __init__(self, client: AuthenticatedClient) -> None: self._client = client {% for endpoint in endpoints %} - def {{ endpoint.name }}(self, {{ arguments_no_client(endpoint) | indent(4)}}) -> {{ endpoint.response_type() }}: + {% if endpoint.requires_security %} + def {{ endpoint.name }}(self, {{ arguments_no_client(endpoint) | indent(4)}}) -> {{ endpoint.success_response_type() }}: response = {{ endpoint.name }}.sync( {{ arguments_passing(endpoint) | indent(8)}} ) if response is None: raise Exception("No response") + assert isinstance(response, {{ endpoint.success_response_type() }}) return response + {% endif %} + {% endfor %} + + +class SyncOperations: + def __init__(self, client: Client) -> None: + self._client = client + {% for endpoint in endpoints %} + {% if endpoint.requires_security %} + + {% else %} + def {{ endpoint.name }}(self, {{ arguments_no_client(endpoint) | indent(4)}}) -> {{ endpoint.success_response_type() }}: + response = {{ endpoint.name }}.sync( + {{ arguments_passing(endpoint) | indent(8)}} + ) + if response is None: + raise Exception("No response") + + assert isinstance(response, {{ endpoint.success_response_type() }}) + return response + {% endif %} {% endfor %} From 194b1da571dcba8380ef41833db2636d7f2a3a8a Mon Sep 17 00:00:00 2001 From: Prawal Gangwar Date: Wed, 28 Dec 2022 23:59:44 +0530 Subject: [PATCH 4/4] add docstrings --- openapi_python_client/templates/wrapper.py.jinja | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openapi_python_client/templates/wrapper.py.jinja b/openapi_python_client/templates/wrapper.py.jinja index bbc8e6685..889db820b 100644 --- a/openapi_python_client/templates/wrapper.py.jinja +++ b/openapi_python_client/templates/wrapper.py.jinja @@ -21,6 +21,7 @@ class SyncAuthenticatedOperations: {% for endpoint in endpoints %} {% if endpoint.requires_security %} def {{ endpoint.name }}(self, {{ arguments_no_client(endpoint) | indent(4)}}) -> {{ endpoint.success_response_type() }}: + {{ docstring(endpoint, endpoint.success_response_type()) | indent(4) }} response = {{ endpoint.name }}.sync( {{ arguments_passing(endpoint) | indent(8)}} ) @@ -42,6 +43,8 @@ class SyncOperations: {% else %} def {{ endpoint.name }}(self, {{ arguments_no_client(endpoint) | indent(4)}}) -> {{ endpoint.success_response_type() }}: + {{ docstring(endpoint, endpoint.success_response_type()) | indent(4) }} + response = {{ endpoint.name }}.sync( {{ arguments_passing(endpoint) | indent(8)}} )