diff --git a/examples/petstore/client_async.py b/examples/petstore/client_async.py index 62d620e..58b54d7 100644 --- a/examples/petstore/client_async.py +++ b/examples/petstore/client_async.py @@ -7,7 +7,7 @@ # # Generator info: # GitHub Page: https://github.com/artsmolin/pythogen -# Version: 0.2.30 +# Version: 0.2.31 # ============================================================================== # jinja2: lstrip_blocks: "True" diff --git a/examples/petstore/client_sync.py b/examples/petstore/client_sync.py index b10c4a3..e2976ee 100644 --- a/examples/petstore/client_sync.py +++ b/examples/petstore/client_sync.py @@ -7,7 +7,7 @@ # # Generator info: # GitHub Page: https://github.com/artsmolin/pythogen -# Version: 0.2.30 +# Version: 0.2.31 # ============================================================================== # jinja2: lstrip_blocks: "True" diff --git a/pyproject.toml b/pyproject.toml index 02ea7ac..2b1032a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pythogen" -version = "0.2.30" +version = "0.2.31" description = "Generator of python HTTP-clients from OpenApi specification." homepage = "https://github.com/artsmolin/pythogen" repository = "https://github.com/artsmolin/pythogen" diff --git a/pythogen/parsers/parameters.py b/pythogen/parsers/parameters.py index a865edc..d557824 100644 --- a/pythogen/parsers/parameters.py +++ b/pythogen/parsers/parameters.py @@ -1,6 +1,8 @@ import re from typing import Any +from pythogen import console +from pythogen import exceptions from pythogen import models from pythogen.parsers.references import RefResolver from pythogen.parsers.schemas import SchemaParser @@ -39,6 +41,14 @@ def parse_item(self, id_: str, data: dict[str, Any]) -> models.ParameterObject: match = re.search(r"(__safety_key__)\((?P.+)\)", description) safety_key = match['safety_key'] if match else None + if len(schema.all_of) > 1: + console.print_error( + title="Failed to generate a client", + msg="\"allOf\" field in property can contains only one item.", + invalid_data=data, + ) + raise exceptions.Exit() + return models.ParameterObject( id=id_, orig_key=data['name'], diff --git a/pythogen/renderer.py b/pythogen/renderer.py index 81e1d7c..787d5c7 100644 --- a/pythogen/renderer.py +++ b/pythogen/renderer.py @@ -269,6 +269,9 @@ def j2_typerepr(schema: models.SchemaObject, document: models.Document) -> str: elif schema.discriminator: representation = " | ".join((j2_typerepr(item, document) for item in schema.discriminator.mapping.values())) + if schema.all_of and len(schema.all_of) == 1: + representation = j2_typerepr(schema.all_of[0], document) + return representation diff --git a/tests/clients/async_client.py b/tests/clients/async_client.py index b8fd4ea..ab13eab 100644 --- a/tests/clients/async_client.py +++ b/tests/clients/async_client.py @@ -7,7 +7,7 @@ # # Generator info: # GitHub Page: https://github.com/artsmolin/pythogen -# Version: 0.2.30 +# Version: 0.2.31 # ============================================================================== # jinja2: lstrip_blocks: "True" @@ -181,6 +181,14 @@ class EmptyBody(BaseModel): text: str +class GetMessageHeaders(BaseModel): + model_config = ConfigDict( + populate_by_name=True, # Addressing by field name, even if there is an alias. + ) + + x_auth_token: str = Field(alias="X-Auth-Token") + + class GetObjectNoRefSchemaPathParams(BaseModel): model_config = ConfigDict( populate_by_name=True, # Addressing by field name, even if there is an alias. @@ -671,6 +679,19 @@ class GetObjectResp(BaseModel): animal: AnimalObj | None = None +class GetMessageResp(BaseModel): + """ + GetMessageResp + + """ + + model_config = ConfigDict( + populate_by_name=True, # Addressing by field name, even if there is an alias. + ) + title: str | None = None + text: str | None = None + + class Dog(BaseModel): """ Dog @@ -780,6 +801,83 @@ def __init__( self.logs_integration = logs_integration self.client_name = client_name + async def getMessage( + self, + auth: BasicAuth | None = None, + content: str | bytes | None = None, + headers: GetMessageHeaders | dict[str, Any] | None = None, + meta: PythogenMetaBox | None = None, + ) -> GetMessageResp | None: + """ + GET /messages + Operation ID: getMessage + Summary: Get message + Description: None + """ + + method = "get" + + path = "/messages" + + url = f"{self.base_url}{path}" + + params = None + + headers_ = self.headers.copy() + + if isinstance(headers, GetMessageHeaders): + headers_ = headers.model_dump(by_alias=True, exclude_none=True) + elif isinstance(headers, dict): + headers_ = headers + + if auth is None: + auth_ = DEFAULT_AUTH + elif isinstance(auth, httpx.Auth): + auth_ = auth + else: + auth_ = (auth.username, auth.password) + + try: + response = await self.client.request( + method, url, headers=headers_, params=params, content=content, auth=auth_ + ) + except Exception as exc: + if self.metrics_integration: + if self.metrics_integration.shadow_path(): + metrics_path = "/messages" + else: + metrics_path = path + self.metrics_integration.on_request_error(self.client_name, exc, method, metrics_path) + + raise exc + + if self.metrics_integration: + if self.metrics_integration.shadow_path(): + metrics_path = "/messages" + else: + metrics_path = path + self.metrics_integration.on_request_success(self.client_name, response, method, metrics_path) + + req = RequestBox( + client_name=self.client_name, + method=method, + url=url, + params=params, + headers=headers_, + content=content, + ) + + resp = ResponseBox( + status_code=response.status_code, + ) + + if meta: + meta.request = req + meta.response = resp + + if response.status_code == 200: + return GetMessageResp.model_validate(response.json()) + async def get_object_no_ref_schema( self, path_params: GetObjectNoRefSchemaPathParams | dict[str, Any], @@ -2617,6 +2715,7 @@ def _parse_any_of(self, item: dict[str, Any], schema_classes: list[Any]) -> Any: PatchObjectData.model_rebuild() PostObjectData.model_rebuild() GetObjectResp.model_rebuild() +GetMessageResp.model_rebuild() Dog.model_rebuild() DogWithKind.model_rebuild() CatWithKind.model_rebuild() diff --git a/tests/clients/async_client_with_headers.py b/tests/clients/async_client_with_headers.py index 70a8c69..44eb3bc 100644 --- a/tests/clients/async_client_with_headers.py +++ b/tests/clients/async_client_with_headers.py @@ -7,7 +7,7 @@ # # Generator info: # GitHub Page: https://github.com/artsmolin/pythogen -# Version: 0.2.30 +# Version: 0.2.31 # ============================================================================== # jinja2: lstrip_blocks: "True" @@ -181,6 +181,14 @@ class EmptyBody(BaseModel): text: str +class GetMessageHeaders(BaseModel): + model_config = ConfigDict( + populate_by_name=True, # Addressing by field name, even if there is an alias. + ) + + x_auth_token: str = Field(alias="X-Auth-Token") + + class GetObjectNoRefSchemaPathParams(BaseModel): model_config = ConfigDict( populate_by_name=True, # Addressing by field name, even if there is an alias. @@ -671,6 +679,19 @@ class GetObjectResp(BaseModel): animal: AnimalObj | None = None +class GetMessageResp(BaseModel): + """ + GetMessageResp + + """ + + model_config = ConfigDict( + populate_by_name=True, # Addressing by field name, even if there is an alias. + ) + title: str | None = None + text: str | None = None + + class Dog(BaseModel): """ Dog @@ -783,6 +804,83 @@ def __init__( if set(["X-API-KEY", "X-API-SECRET"]) != set(self.headers): raise RequiredHeaders("Headers ['X-API-KEY', 'X-API-SECRET'] is required") + async def getMessage( + self, + auth: BasicAuth | None = None, + content: str | bytes | None = None, + headers: GetMessageHeaders | dict[str, Any] | None = None, + meta: PythogenMetaBox | None = None, + ) -> GetMessageResp | None: + """ + GET /messages + Operation ID: getMessage + Summary: Get message + Description: None + """ + + method = "get" + + path = "/messages" + + url = f"{self.base_url}{path}" + + params = None + + headers_ = self.headers.copy() + + if isinstance(headers, GetMessageHeaders): + headers_ = headers.model_dump(by_alias=True, exclude_none=True) + elif isinstance(headers, dict): + headers_ = headers + + if auth is None: + auth_ = DEFAULT_AUTH + elif isinstance(auth, httpx.Auth): + auth_ = auth + else: + auth_ = (auth.username, auth.password) + + try: + response = await self.client.request( + method, url, headers=headers_, params=params, content=content, auth=auth_ + ) + except Exception as exc: + if self.metrics_integration: + if self.metrics_integration.shadow_path(): + metrics_path = "/messages" + else: + metrics_path = path + self.metrics_integration.on_request_error(self.client_name, exc, method, metrics_path) + + raise exc + + if self.metrics_integration: + if self.metrics_integration.shadow_path(): + metrics_path = "/messages" + else: + metrics_path = path + self.metrics_integration.on_request_success(self.client_name, response, method, metrics_path) + + req = RequestBox( + client_name=self.client_name, + method=method, + url=url, + params=params, + headers=headers_, + content=content, + ) + + resp = ResponseBox( + status_code=response.status_code, + ) + + if meta: + meta.request = req + meta.response = resp + + if response.status_code == 200: + return GetMessageResp.model_validate(response.json()) + async def get_object_no_ref_schema( self, path_params: GetObjectNoRefSchemaPathParams | dict[str, Any], @@ -2620,6 +2718,7 @@ def _parse_any_of(self, item: dict[str, Any], schema_classes: list[Any]) -> Any: PatchObjectData.model_rebuild() PostObjectData.model_rebuild() GetObjectResp.model_rebuild() +GetMessageResp.model_rebuild() Dog.model_rebuild() DogWithKind.model_rebuild() CatWithKind.model_rebuild() diff --git a/tests/clients/async_client_with_metrics.py b/tests/clients/async_client_with_metrics.py index d947a1f..51b7ec5 100644 --- a/tests/clients/async_client_with_metrics.py +++ b/tests/clients/async_client_with_metrics.py @@ -7,7 +7,7 @@ # # Generator info: # GitHub Page: https://github.com/artsmolin/pythogen -# Version: 0.2.30 +# Version: 0.2.31 # ============================================================================== # jinja2: lstrip_blocks: "True" @@ -213,6 +213,14 @@ class EmptyBody(BaseModel): text: str +class GetMessageHeaders(BaseModel): + model_config = ConfigDict( + populate_by_name=True, # Addressing by field name, even if there is an alias. + ) + + x_auth_token: str = Field(alias="X-Auth-Token") + + class GetObjectNoRefSchemaPathParams(BaseModel): model_config = ConfigDict( populate_by_name=True, # Addressing by field name, even if there is an alias. @@ -703,6 +711,19 @@ class GetObjectResp(BaseModel): animal: AnimalObj | None = None +class GetMessageResp(BaseModel): + """ + GetMessageResp + + """ + + model_config = ConfigDict( + populate_by_name=True, # Addressing by field name, even if there is an alias. + ) + title: str | None = None + text: str | None = None + + class Dog(BaseModel): """ Dog @@ -812,6 +833,83 @@ def __init__( self.logs_integration = logs_integration self.client_name = client_name + async def getMessage( + self, + auth: BasicAuth | None = None, + content: str | bytes | None = None, + headers: GetMessageHeaders | dict[str, Any] | None = None, + meta: PythogenMetaBox | None = None, + ) -> GetMessageResp | None: + """ + GET /messages + Operation ID: getMessage + Summary: Get message + Description: None + """ + + method = "get" + + path = "/messages" + + url = f"{self.base_url}{path}" + + params = None + + headers_ = self.headers.copy() + + if isinstance(headers, GetMessageHeaders): + headers_ = headers.model_dump(by_alias=True, exclude_none=True) + elif isinstance(headers, dict): + headers_ = headers + + if auth is None: + auth_ = DEFAULT_AUTH + elif isinstance(auth, httpx.Auth): + auth_ = auth + else: + auth_ = (auth.username, auth.password) + + try: + response = await self.client.request( + method, url, headers=headers_, params=params, content=content, auth=auth_ + ) + except Exception as exc: + if self.metrics_integration: + if self.metrics_integration.shadow_path(): + metrics_path = "/messages" + else: + metrics_path = path + self.metrics_integration.on_request_error(self.client_name, exc, method, metrics_path) + + raise exc + + if self.metrics_integration: + if self.metrics_integration.shadow_path(): + metrics_path = "/messages" + else: + metrics_path = path + self.metrics_integration.on_request_success(self.client_name, response, method, metrics_path) + + req = RequestBox( + client_name=self.client_name, + method=method, + url=url, + params=params, + headers=headers_, + content=content, + ) + + resp = ResponseBox( + status_code=response.status_code, + ) + + if meta: + meta.request = req + meta.response = resp + + if response.status_code == 200: + return GetMessageResp.model_validate(response.json()) + async def get_object_no_ref_schema( self, path_params: GetObjectNoRefSchemaPathParams | dict[str, Any], @@ -2649,6 +2747,7 @@ def _parse_any_of(self, item: dict[str, Any], schema_classes: list[Any]) -> Any: PatchObjectData.model_rebuild() PostObjectData.model_rebuild() GetObjectResp.model_rebuild() +GetMessageResp.model_rebuild() Dog.model_rebuild() DogWithKind.model_rebuild() CatWithKind.model_rebuild() diff --git a/tests/clients/sync_client.py b/tests/clients/sync_client.py index f473959..ea0239a 100644 --- a/tests/clients/sync_client.py +++ b/tests/clients/sync_client.py @@ -7,7 +7,7 @@ # # Generator info: # GitHub Page: https://github.com/artsmolin/pythogen -# Version: 0.2.30 +# Version: 0.2.31 # ============================================================================== # jinja2: lstrip_blocks: "True" @@ -181,6 +181,14 @@ class EmptyBody(BaseModel): text: str +class GetMessageHeaders(BaseModel): + model_config = ConfigDict( + populate_by_name=True, # Addressing by field name, even if there is an alias. + ) + + x_auth_token: str = Field(alias="X-Auth-Token") + + class GetObjectNoRefSchemaPathParams(BaseModel): model_config = ConfigDict( populate_by_name=True, # Addressing by field name, even if there is an alias. @@ -671,6 +679,19 @@ class GetObjectResp(BaseModel): animal: AnimalObj | None = None +class GetMessageResp(BaseModel): + """ + GetMessageResp + + """ + + model_config = ConfigDict( + populate_by_name=True, # Addressing by field name, even if there is an alias. + ) + title: str | None = None + text: str | None = None + + class Dog(BaseModel): """ Dog @@ -780,6 +801,81 @@ def __init__( self.logs_integration = logs_integration self.client_name = client_name + def getMessage( + self, + auth: BasicAuth | None = None, + content: str | bytes | None = None, + headers: GetMessageHeaders | dict[str, Any] | None = None, + meta: PythogenMetaBox | None = None, + ) -> GetMessageResp | None: + """ + GET /messages + Operation ID: getMessage + Summary: Get message + Description: None + """ + + method = "get" + + path = "/messages" + + url = f"{self.base_url}{path}" + + params = None + + headers_ = self.headers.copy() + + if isinstance(headers, GetMessageHeaders): + headers_ = headers.model_dump(by_alias=True, exclude_none=True) + elif isinstance(headers, dict): + headers_ = headers + + if auth is None: + auth_ = DEFAULT_AUTH + elif isinstance(auth, httpx.Auth): + auth_ = auth + else: + auth_ = (auth.username, auth.password) + + try: + response = self.client.request(method, url, headers=headers_, params=params, content=content, auth=auth_) + except Exception as exc: + if self.metrics_integration: + if self.metrics_integration.shadow_path(): + metrics_path = "/messages" + else: + metrics_path = path + self.metrics_integration.on_request_error(self.client_name, exc, method, metrics_path) + + raise exc + + if self.metrics_integration: + if self.metrics_integration.shadow_path(): + metrics_path = "/messages" + else: + metrics_path = path + self.metrics_integration.on_request_success(self.client_name, response, method, metrics_path) + + req = RequestBox( + client_name=self.client_name, + method=method, + url=url, + params=params, + headers=headers_, + content=content, + ) + + resp = ResponseBox( + status_code=response.status_code, + ) + + if meta: + meta.request = req + meta.response = resp + + if response.status_code == 200: + return GetMessageResp.model_validate(response.json()) + def get_object_no_ref_schema( self, path_params: GetObjectNoRefSchemaPathParams | dict[str, Any], @@ -2585,6 +2681,7 @@ def _parse_any_of(self, item: dict[str, Any], schema_classes: list[Any]) -> Any: PatchObjectData.model_rebuild() PostObjectData.model_rebuild() GetObjectResp.model_rebuild() +GetMessageResp.model_rebuild() Dog.model_rebuild() DogWithKind.model_rebuild() CatWithKind.model_rebuild() diff --git a/tests/clients/sync_client_with_metrics.py b/tests/clients/sync_client_with_metrics.py index 1cdb7b4..da3ec7f 100644 --- a/tests/clients/sync_client_with_metrics.py +++ b/tests/clients/sync_client_with_metrics.py @@ -7,7 +7,7 @@ # # Generator info: # GitHub Page: https://github.com/artsmolin/pythogen -# Version: 0.2.30 +# Version: 0.2.31 # ============================================================================== # jinja2: lstrip_blocks: "True" @@ -213,6 +213,14 @@ class EmptyBody(BaseModel): text: str +class GetMessageHeaders(BaseModel): + model_config = ConfigDict( + populate_by_name=True, # Addressing by field name, even if there is an alias. + ) + + x_auth_token: str = Field(alias="X-Auth-Token") + + class GetObjectNoRefSchemaPathParams(BaseModel): model_config = ConfigDict( populate_by_name=True, # Addressing by field name, even if there is an alias. @@ -703,6 +711,19 @@ class GetObjectResp(BaseModel): animal: AnimalObj | None = None +class GetMessageResp(BaseModel): + """ + GetMessageResp + + """ + + model_config = ConfigDict( + populate_by_name=True, # Addressing by field name, even if there is an alias. + ) + title: str | None = None + text: str | None = None + + class Dog(BaseModel): """ Dog @@ -812,6 +833,81 @@ def __init__( self.logs_integration = logs_integration self.client_name = client_name + def getMessage( + self, + auth: BasicAuth | None = None, + content: str | bytes | None = None, + headers: GetMessageHeaders | dict[str, Any] | None = None, + meta: PythogenMetaBox | None = None, + ) -> GetMessageResp | None: + """ + GET /messages + Operation ID: getMessage + Summary: Get message + Description: None + """ + + method = "get" + + path = "/messages" + + url = f"{self.base_url}{path}" + + params = None + + headers_ = self.headers.copy() + + if isinstance(headers, GetMessageHeaders): + headers_ = headers.model_dump(by_alias=True, exclude_none=True) + elif isinstance(headers, dict): + headers_ = headers + + if auth is None: + auth_ = DEFAULT_AUTH + elif isinstance(auth, httpx.Auth): + auth_ = auth + else: + auth_ = (auth.username, auth.password) + + try: + response = self.client.request(method, url, headers=headers_, params=params, content=content, auth=auth_) + except Exception as exc: + if self.metrics_integration: + if self.metrics_integration.shadow_path(): + metrics_path = "/messages" + else: + metrics_path = path + self.metrics_integration.on_request_error(self.client_name, exc, method, metrics_path) + + raise exc + + if self.metrics_integration: + if self.metrics_integration.shadow_path(): + metrics_path = "/messages" + else: + metrics_path = path + self.metrics_integration.on_request_success(self.client_name, response, method, metrics_path) + + req = RequestBox( + client_name=self.client_name, + method=method, + url=url, + params=params, + headers=headers_, + content=content, + ) + + resp = ResponseBox( + status_code=response.status_code, + ) + + if meta: + meta.request = req + meta.response = resp + + if response.status_code == 200: + return GetMessageResp.model_validate(response.json()) + def get_object_no_ref_schema( self, path_params: GetObjectNoRefSchemaPathParams | dict[str, Any], @@ -2617,6 +2713,7 @@ def _parse_any_of(self, item: dict[str, Any], schema_classes: list[Any]) -> Any: PatchObjectData.model_rebuild() PostObjectData.model_rebuild() GetObjectResp.model_rebuild() +GetMessageResp.model_rebuild() Dog.model_rebuild() DogWithKind.model_rebuild() CatWithKind.model_rebuild() diff --git a/tests/docs/openapi.yaml b/tests/docs/openapi.yaml index e2fd212..f1a5c47 100644 --- a/tests/docs/openapi.yaml +++ b/tests/docs/openapi.yaml @@ -3,6 +3,29 @@ info: title: test server version: 0.1.0 paths: + /messages: + get: + tags: + - test + summary: Get message + operationId: getMessage + parameters: + - description: Auth token + in: header + name: X-Auth-Token + required: true + schema: + allOf: + - {$ref: '#/components/schemas/Token'} + title: Auth Token + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GetMessageResp' + /objects/no-ref-schema/{object_id}: get: tags: @@ -477,6 +500,9 @@ paths: $ref: '#/components/schemas/ListAnyOfResp' components: schemas: + Token: + title: Token + type: string AllOfResp: title: All Of Resp type: object @@ -570,6 +596,16 @@ components: - {$ref: '#/components/schemas/Dog'} - {$ref: '#/components/schemas/Cat'} - type: integer + GetMessageResp: + title: GetMessageResp + type: object + properties: + title: + title: Title + type: string + text: + title: Text + type: string GetObjectResp: title: GetObjectResp type: object diff --git a/tests/pythogen/test_clients.py b/tests/pythogen/test_clients.py index ad8883a..f70a215 100644 --- a/tests/pythogen/test_clients.py +++ b/tests/pythogen/test_clients.py @@ -197,4 +197,9 @@ async def test_httpx_async_client(): assert isinstance(response.required_discriminated_animal, async_client.DogWithKind) assert response.discriminated_animal is None + response = await httpx_async_client.getMessage( + headers=async_client.GetMessageHeaders(x_auth_token="qwerty12345!"), + ) + assert isinstance(response, async_client.GetMessageResp) + await httpx_async_client.close() diff --git a/tests/server/server.py b/tests/server/server.py index 5bc0db9..a9c365c 100644 --- a/tests/server/server.py +++ b/tests/server/server.py @@ -131,6 +131,9 @@ async def get_list_of_anyof(request: web.Request) -> web.Response: async def get_discriminated_oneof(request: web.Request) -> web.Response: return web.json_response(data={'required_discriminated_animal': {'kind': 'dog', 'name': 'Puppy'}}) +async def get_message(request: web.Request) -> web.Response: + return web.json_response(data={'title': 'Hello', 'text': 'World'}) + app = web.Application() app.add_routes( @@ -152,6 +155,7 @@ async def get_discriminated_oneof(request: web.Request) -> web.Response: web.put('/slow/objects/{object_id}', put_object_slow), web.get('/nested-any-of', get_list_of_anyof), web.get('/discriminated-oneof', get_discriminated_oneof), + web.get('/messages', get_message), ] )