Skip to content

Commit 636b806

Browse files
authored
Merge pull request #12 from mattcoulter7/feat/httpxtimeout
feat: support httpx timeout
2 parents 0ceeed7 + 1e5879c commit 636b806

4 files changed

Lines changed: 35 additions & 15 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pydantic-openapi-generator"
3-
version = "2.6.0"
3+
version = "2.7.0"
44
description = "Openapi Python Generator"
55
authors = [
66
{ name = "Marco Müllner", email = "muellnermarco@gmail.com" },

src/pydantic_openapi_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@ from typing import Any, Dict, Optional, Union
77
import json
88

99
import httpx
10-
from pydantic import BaseModel, SecretStr, TypeAdapter
10+
from pydantic import BaseModel, Field, SecretStr, TypeAdapter
1111

1212
from ..models import *
1313

1414
{% set _base_url = servers[0].url if servers|length > 0 else "/" %}
1515

1616

1717
class AsyncClient(BaseModel):
18-
model_config = {"validate_assignment": True}
18+
model_config = {"validate_assignment": True, "arbitrary_types_allowed": True}
1919

2020
base_url: str = "{{ _base_url }}"
2121
verify: Union[bool, str] = True
22+
timeout: Union[float, httpx.Timeout, None] = Field(default_factory=lambda: httpx.Timeout(5.0))
2223
{% if env_token_name is none %}
2324
access_token: Optional[str] = None
2425
{% endif %}
@@ -100,7 +101,7 @@ AsyncGenerator[str | {% if op.sse_data_handler and op.sse_data_handler.type %}{%
100101
query_params = {k: v for (k, v) in query_params.items() if v is not None}
101102

102103
{% if op.is_sse %}
103-
async with httpx.AsyncClient(base_url=base_url, verify=self.verify) as client:
104+
async with httpx.AsyncClient(base_url=base_url, verify=self.verify, timeout=self.timeout) as client:
104105
async with client.stream(
105106
"{{ op.method }}",
106107
httpx.URL(path),
@@ -148,7 +149,7 @@ AsyncGenerator[str | {% if op.sse_data_handler and op.sse_data_handler.type %}{%
148149
# Non-data lines: yield as raw text for debugging/visibility
149150
yield line
150151
{% else %}
151-
async with httpx.AsyncClient(base_url=base_url, verify=self.verify) as client:
152+
async with httpx.AsyncClient(base_url=base_url, verify=self.verify, timeout=self.timeout) as client:
152153
response = await client.request(
153154
"{{ op.method }}",
154155
httpx.URL(path),

src/pydantic_openapi_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@ from typing import Any, Dict, Optional, Union
77
import json
88

99
import httpx
10-
from pydantic import BaseModel, SecretStr, TypeAdapter
10+
from pydantic import BaseModel, Field, SecretStr, TypeAdapter
1111

1212
from ..models import *
1313

1414
{% set _base_url = servers[0].url if servers|length > 0 else "/" %}
1515

1616

1717
class SyncClient(BaseModel):
18-
model_config = {"validate_assignment": True}
18+
model_config = {"validate_assignment": True, "arbitrary_types_allowed": True}
1919

2020
base_url: str = "{{ _base_url }}"
2121
verify: Union[bool, str] = True
22+
timeout: Union[float, httpx.Timeout, None] = Field(default_factory=lambda: httpx.Timeout(5.0))
2223
{% if env_token_name is none %}
2324
access_token: Optional[str] = None
2425
{% endif %}
@@ -100,7 +101,7 @@ Generator[str | {% if op.sse_data_handler and op.sse_data_handler.type %}{% if o
100101
query_params = {k: v for (k, v) in query_params.items() if v is not None}
101102

102103
{% if op.is_sse %}
103-
with httpx.Client(base_url=base_url, verify=self.verify) as client:
104+
with httpx.Client(base_url=base_url, verify=self.verify, timeout=self.timeout) as client:
104105
with client.stream(
105106
"{{ op.method }}",
106107
httpx.URL(path),
@@ -147,7 +148,7 @@ Generator[str | {% if op.sse_data_handler and op.sse_data_handler.type %}{% if o
147148
else:
148149
yield line
149150
{% else %}
150-
with httpx.Client(base_url=base_url, verify=self.verify) as client:
151+
with httpx.Client(base_url=base_url, verify=self.verify, timeout=self.timeout) as client:
151152
response = client.request(
152153
"{{ op.method }}",
153154
httpx.URL(path),

tests/test_client_generator_contracts.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,13 @@ def download_handler(request: httpx.Request) -> httpx.Response:
264264
client_module = importlib.import_module(
265265
f"{generated_contract_package}.clients.async_client"
266266
)
267-
client = client_module.AsyncClient()
267+
client = client_module.AsyncClient(timeout=1.23)
268268
thing, upload, download, empty = asyncio.run(_run_async_contract(client))
269269
else:
270270
client_module = importlib.import_module(
271271
f"{generated_contract_package}.clients.sync_client"
272272
)
273-
client = client_module.SyncClient()
273+
client = client_module.SyncClient(timeout=1.23)
274274
thing = client.getThing(X_Test_Header="required")
275275
upload = client.uploadDocument(
276276
data={"file": ("doc.txt", b"hello", "text/plain")}
@@ -287,6 +287,12 @@ def download_handler(request: httpx.Request) -> httpx.Response:
287287
assert thing_request.headers["X-Test-Header"] == "required"
288288
assert thing_request.headers["X-Optional-Header"] == "AU"
289289
assert thing_request.url.params["brand"] == "NRMA"
290+
assert thing_request.extensions["timeout"] == {
291+
"connect": 1.23,
292+
"read": 1.23,
293+
"write": 1.23,
294+
"pool": 1.23,
295+
}
290296

291297
upload_request = requests[1]
292298
assert upload_request.headers["content-type"].startswith("multipart/form-data")
@@ -431,8 +437,11 @@ def test_generated_sse_data_payloads_use_declared_schema(
431437
respx_mock,
432438
async_client,
433439
):
434-
respx_mock.get("http://testserver/events").mock(
435-
return_value=httpx.Response(
440+
requests: list[httpx.Request] = []
441+
442+
def events_handler(request: httpx.Request) -> httpx.Response:
443+
requests.append(request)
444+
return httpx.Response(
436445
200,
437446
content=(
438447
b"event: update\n"
@@ -441,27 +450,36 @@ def test_generated_sse_data_payloads_use_declared_schema(
441450
),
442451
headers={"content-type": "text/event-stream"},
443452
)
453+
454+
respx_mock.get("http://testserver/events").mock(
455+
side_effect=events_handler
444456
)
445457
models = importlib.import_module(f"{generated_contract_package}.models")
446458

447459
if async_client:
448460
client_module = importlib.import_module(
449461
f"{generated_contract_package}.clients.async_client"
450462
)
451-
client = client_module.AsyncClient()
463+
client = client_module.AsyncClient(timeout=2.34)
452464
first_data_item = asyncio.run(_first_async_sse_data_item(client))
453465
else:
454466
client_module = importlib.import_module(
455467
f"{generated_contract_package}.clients.sync_client"
456468
)
457-
client = client_module.SyncClient()
469+
client = client_module.SyncClient(timeout=2.34)
458470
first_data_item = next(
459471
item for item in client.getEvents() if not isinstance(item, str)
460472
)
461473

462474
assert isinstance(first_data_item, models.EventPayload)
463475
assert first_data_item.message == "ok"
464476
assert first_data_item.value == 42.5
477+
assert requests[0].extensions["timeout"] == {
478+
"connect": 2.34,
479+
"read": 2.34,
480+
"write": 2.34,
481+
"pool": 2.34,
482+
}
465483

466484

467485
async def _run_async_configured_contract(client):

0 commit comments

Comments
 (0)