Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions end_to_end_tests/3.1_specific.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ info:
description: "Test new OpenAPI 3.1 features"
version: "0.1.0"
paths:
"/content/{item_id}":
get:
tags: [ "content" ]
parameters:
- name: "item_id"
in: "path"
required: true
content:
"application/json":
schema:
type: string
enum: ["a", "b", "c"]
responses:
"200":
description: "Successful Response"
content:
"application/json":
schema:
type: string
"/const/{path}":
post:
tags: [ "const" ]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from unittest.mock import MagicMock

import httpx

from end_to_end_tests.functional_tests.helpers import (
with_generated_client_fixture,
with_generated_code_import,
)


@with_generated_client_fixture(
"""
paths:
"/items/{item_id}":
get:
operationId: getItem
parameters:
- name: item_id
in: path
required: true
content:
application/json:
schema:
type: string
enum: [a, b, c]
responses:
"200":
description: Success
content:
application/json:
schema:
type: string
""")
@with_generated_code_import(".api.default.get_item.sync_detailed")
@with_generated_code_import(".client.Client")
class TestContentOnlyPathParameters:
def test_content_only_path_parameter_generates_callable_endpoint(self, sync_detailed, Client):
mock_httpx_client = MagicMock(spec=httpx.Client)
mock_response = MagicMock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.json.return_value = "ok"
mock_response.content = b'"ok"'
mock_response.headers = {}
mock_httpx_client.request.return_value = mock_response

client = Client(base_url="https://api.example.com")
client.set_httpx_client(mock_httpx_client)

response = sync_detailed(item_id="a", client=client)

mock_httpx_client.request.assert_called_once()
call_kwargs = mock_httpx_client.request.call_args[1]
assert call_kwargs["url"] == "/items/a"
assert response.parsed == "ok"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Contains endpoint functions for accessing the API"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
from http import HTTPStatus
from typing import Any, cast
from urllib.parse import quote

import httpx

from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.get_content_item_id_item_id import GetContentItemIdItemId
from ...types import Response


def _get_kwargs(
item_id: GetContentItemIdItemId,
) -> dict[str, Any]:

_kwargs: dict[str, Any] = {
"method": "get",
"url": "/content/{item_id}".format(
item_id=quote(str(item_id), safe=""),
),
}

return _kwargs


def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> str | None:
if response.status_code == 200:
response_200 = cast(str, response.json())
return response_200

if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None


def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[str]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)


def sync_detailed(
item_id: GetContentItemIdItemId,
*,
client: AuthenticatedClient | Client,
) -> Response[str]:
"""
Args:
item_id (GetContentItemIdItemId):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
Response[str]
"""

kwargs = _get_kwargs(
item_id=item_id,
)

response = client.get_httpx_client().request(
**kwargs,
)

return _build_response(client=client, response=response)


def sync(
item_id: GetContentItemIdItemId,
*,
client: AuthenticatedClient | Client,
) -> str | None:
"""
Args:
item_id (GetContentItemIdItemId):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
str
"""

return sync_detailed(
item_id=item_id,
client=client,
).parsed


async def asyncio_detailed(
item_id: GetContentItemIdItemId,
*,
client: AuthenticatedClient | Client,
) -> Response[str]:
"""
Args:
item_id (GetContentItemIdItemId):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
Response[str]
"""

kwargs = _get_kwargs(
item_id=item_id,
)

response = await client.get_async_httpx_client().request(**kwargs)

return _build_response(client=client, response=response)


async def asyncio(
item_id: GetContentItemIdItemId,
*,
client: AuthenticatedClient | Client,
) -> str | None:
"""
Args:
item_id (GetContentItemIdItemId):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
str
"""

return (
await asyncio_detailed(
item_id=item_id,
client=client,
)
).parsed
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Contains all the data models used in inputs/outputs"""

from .get_content_item_id_item_id import GetContentItemIdItemId
from .post_const_path_body import PostConstPathBody
from .post_prefix_items_body import PostPrefixItemsBody

__all__ = (
"GetContentItemIdItemId",
"PostConstPathBody",
"PostPrefixItemsBody",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from enum import Enum


class GetContentItemIdItemId(str, Enum):
A = "a"
B = "b"
C = "c"

def __str__(self) -> str:
return str(self.value)
9 changes: 7 additions & 2 deletions openapi_python_client/parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,12 @@ def add_parameters(
return param_or_error, schemas, parameters
param = param_or_error # noqa: PLW2901

if param.param_schema is None:
param_schema = param.param_schema
if param_schema is None and param.content:
first_media_type = next(iter(param.content.values()))
param_schema = first_media_type.media_type_schema

if param_schema is None:
continue

unique_param = (param.name, param.param_in)
Expand Down Expand Up @@ -278,7 +283,7 @@ def add_parameters(
prop, new_schemas = property_from_data(
name=param.name,
required=param.required,
data=param.param_schema,
data=param_schema,
schemas=schemas,
parent_name=endpoint.name,
config=config,
Expand Down
25 changes: 25 additions & 0 deletions tests/test_parser/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,31 @@ def test__add_parameters_skips_params_without_schemas(self, config):
assert isinstance(endpoint, Endpoint)
assert len(endpoint.path_parameters) == 0

def test__add_parameters_uses_content_schema_when_param_schema_missing(self, config):
endpoint = self.make_endpoint()
data = oai.Operation.model_construct(
parameters=[
oai.Parameter.model_construct(
name="item_id",
required=True,
param_in=oai.ParameterLocation.PATH,
content={
"application/json": oai.MediaType.model_construct(
media_type_schema=oai.Schema.model_construct(type="string")
)
},
),
]
)

(endpoint, _, _) = endpoint.add_parameters(
endpoint=endpoint, data=data, schemas=Schemas(), parameters=Parameters(), config=config
)

assert isinstance(endpoint, Endpoint)
assert len(endpoint.path_parameters) == 1
assert endpoint.path_parameters[0].name == "item_id"

def test__add_parameters_same_identifier_conflict(self, config):
endpoint = self.make_endpoint()
data = oai.Operation.model_construct(
Expand Down