openai/openai-python

Public

mirrored from https://github.com/openai/openai-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.90.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

tests/test_client.py

1887lines · modeblame

5cfb125aStainless Bot2 years ago1# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
08b8179aDavid Schnurr2 years ago2
3from __future__ import annotations
4
d052708aStainless Bot2 years ago5import gc
08b8179aDavid Schnurr2 years ago6import os
d8901d28Seth Gilchrist1 years ago7import sys
08b8179aDavid Schnurr2 years ago8import json
14543c59stainless-app[bot]1 years ago9import time
08b8179aDavid Schnurr2 years ago10import asyncio
11import inspect
d8901d28Seth Gilchrist1 years ago12import subprocess
d052708aStainless Bot2 years ago13import tracemalloc
31573844Stainless Bot2 years ago14from typing import Any, Union, cast
d8901d28Seth Gilchrist1 years ago15from textwrap import dedent
08b8179aDavid Schnurr2 years ago16from unittest import mock
6634f525stainless-app[bot]1 years ago17from typing_extensions import Literal
08b8179aDavid Schnurr2 years ago18
19import httpx
20import pytest
21from respx import MockRouter
22from pydantic import ValidationError
23
24from openai import OpenAI, AsyncOpenAI, APIResponseValidationError
30194f19stainless-app[bot]1 years ago25from openai._types import Omit
08b8179aDavid Schnurr2 years ago26from openai._models import BaseModel, FinalRequestOptions
27from openai._streaming import Stream, AsyncStream
a47375b7Stainless Bot2 years ago28from openai._exceptions import OpenAIError, APIStatusError, APITimeoutError, APIResponseValidationError
cc2c1fc1stainless-app[bot]1 years ago29from openai._base_client import (
30DEFAULT_TIMEOUT,
31HTTPX_DEFAULT_TIMEOUT,
32BaseClient,
33DefaultHttpxClient,
34DefaultAsyncHttpxClient,
35make_request_options,
36)
08b8179aDavid Schnurr2 years ago37
0733934fStainless Bot2 years ago38from .utils import update_env
39
08b8179aDavid Schnurr2 years ago40base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
41api_key = "My API Key"
42
43
44def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]:
45request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
46url = httpx.URL(request.url)
47return dict(url.params)
48
49
ba4f7a97Stainless Bot2 years ago50def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float:
51return 0.1
7aad3405Stainless Bot2 years ago52
53
54def _get_open_connections(client: OpenAI | AsyncOpenAI) -> int:
55transport = client._client._transport
56assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport)
57
58pool = transport._pool
59return len(pool._requests)
60
61
08b8179aDavid Schnurr2 years ago62class TestOpenAI:
63client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
64
65@pytest.mark.respx(base_url=base_url)
66def test_raw_response(self, respx_mock: MockRouter) -> None:
c5975bd0Stainless Bot2 years ago67respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago68
69response = self.client.post("/foo", cast_to=httpx.Response)
70assert response.status_code == 200
71assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago72assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago73
74@pytest.mark.respx(base_url=base_url)
75def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
76respx_mock.post("/foo").mock(
77return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
78)
79
80response = self.client.post("/foo", cast_to=httpx.Response)
81assert response.status_code == 200
82assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago83assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago84
85def test_copy(self) -> None:
86copied = self.client.copy()
87assert id(copied) != id(self.client)
88
89copied = self.client.copy(api_key="another My API Key")
90assert copied.api_key == "another My API Key"
91assert self.client.api_key == "My API Key"
92
93def test_copy_default_options(self) -> None:
94# options that have a default are overridden correctly
95copied = self.client.copy(max_retries=7)
96assert copied.max_retries == 7
97assert self.client.max_retries == 2
98
99copied2 = copied.copy(max_retries=6)
100assert copied2.max_retries == 6
101assert copied.max_retries == 7
102
103# timeout
104assert isinstance(self.client.timeout, httpx.Timeout)
105copied = self.client.copy(timeout=None)
106assert copied.timeout is None
107assert isinstance(self.client.timeout, httpx.Timeout)
108
109def test_copy_default_headers(self) -> None:
110client = OpenAI(
111base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
112)
113assert client.default_headers["X-Foo"] == "bar"
114
115# does not override the already given value when not specified
116copied = client.copy()
117assert copied.default_headers["X-Foo"] == "bar"
118
119# merges already given headers
120copied = client.copy(default_headers={"X-Bar": "stainless"})
121assert copied.default_headers["X-Foo"] == "bar"
122assert copied.default_headers["X-Bar"] == "stainless"
123
124# uses new values for any already given headers
125copied = client.copy(default_headers={"X-Foo": "stainless"})
126assert copied.default_headers["X-Foo"] == "stainless"
127
128# set_default_headers
129
130# completely overrides already set values
131copied = client.copy(set_default_headers={})
132assert copied.default_headers.get("X-Foo") is None
133
134copied = client.copy(set_default_headers={"X-Bar": "Robert"})
135assert copied.default_headers["X-Bar"] == "Robert"
136
137with pytest.raises(
138ValueError,
139match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
140):
141client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
142
143def test_copy_default_query(self) -> None:
144client = OpenAI(
145base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
146)
147assert _get_params(client)["foo"] == "bar"
148
149# does not override the already given value when not specified
150copied = client.copy()
151assert _get_params(copied)["foo"] == "bar"
152
153# merges already given params
154copied = client.copy(default_query={"bar": "stainless"})
155params = _get_params(copied)
156assert params["foo"] == "bar"
157assert params["bar"] == "stainless"
158
159# uses new values for any already given headers
160copied = client.copy(default_query={"foo": "stainless"})
161assert _get_params(copied)["foo"] == "stainless"
162
163# set_default_query
164
165# completely overrides already set values
166copied = client.copy(set_default_query={})
167assert _get_params(copied) == {}
168
169copied = client.copy(set_default_query={"bar": "Robert"})
170assert _get_params(copied)["bar"] == "Robert"
171
172with pytest.raises(
173ValueError,
174# TODO: update
175match="`default_query` and `set_default_query` arguments are mutually exclusive",
176):
177client.copy(set_default_query={}, default_query={"foo": "Bar"})
178
179def test_copy_signature(self) -> None:
180# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
181init_signature = inspect.signature(
182# mypy doesn't like that we access the `__init__` property.
183self.client.__init__, # type: ignore[misc]
184)
185copy_signature = inspect.signature(self.client.copy)
186exclude_params = {"transport", "proxies", "_strict_response_validation"}
187
188for name in init_signature.parameters.keys():
189if name in exclude_params:
190continue
191
192copy_param = copy_signature.parameters.get(name)
193assert copy_param is not None, f"copy() signature is missing the {name} param"
194
d052708aStainless Bot2 years ago195def test_copy_build_request(self) -> None:
196options = FinalRequestOptions(method="get", url="/foo")
197
198def build_request(options: FinalRequestOptions) -> None:
199client = self.client.copy()
200client._build_request(options)
201
202# ensure that the machinery is warmed up before tracing starts.
203build_request(options)
204gc.collect()
205
206tracemalloc.start(1000)
207
208snapshot_before = tracemalloc.take_snapshot()
209
210ITERATIONS = 10
211for _ in range(ITERATIONS):
212build_request(options)
213
ce04ec28Stainless Bot2 years ago214gc.collect()
d052708aStainless Bot2 years ago215snapshot_after = tracemalloc.take_snapshot()
216
217tracemalloc.stop()
218
219def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
220if diff.count == 0:
221# Avoid false positives by considering only leaks (i.e. allocations that persist).
222return
223
224if diff.count % ITERATIONS != 0:
225# Avoid false positives by considering only leaks that appear per iteration.
226return
227
228for frame in diff.traceback:
229if any(
230frame.filename.endswith(fragment)
231for fragment in [
232# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
233#
234# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago235"openai/_legacy_response.py",
d052708aStainless Bot2 years ago236"openai/_response.py",
237# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
238"openai/_compat.py",
239# Standard library leaks we don't care about.
240"/logging/__init__.py",
241]
242):
243return
244
245leaks.append(diff)
246
247leaks: list[tracemalloc.StatisticDiff] = []
248for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
249add_leak(leaks, diff)
250if leaks:
251for leak in leaks:
252print("MEMORY LEAK:", leak)
253for frame in leak.traceback:
254print(frame)
255raise AssertionError()
256
08b8179aDavid Schnurr2 years ago257def test_request_timeout(self) -> None:
258request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
259timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
260assert timeout == DEFAULT_TIMEOUT
261
262request = self.client._build_request(
263FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
264)
265timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
266assert timeout == httpx.Timeout(100.0)
267
268def test_client_timeout_option(self) -> None:
269client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0))
270
271request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
272timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
273assert timeout == httpx.Timeout(0)
274
275def test_http_client_timeout_option(self) -> None:
276# custom timeout given to the httpx client should be used
277with httpx.Client(timeout=None) as http_client:
278client = OpenAI(
279base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
280)
281
282request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
283timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
284assert timeout == httpx.Timeout(None)
285
286# no timeout given to the httpx client should not use the httpx default
287with httpx.Client() as http_client:
288client = OpenAI(
289base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
290)
291
292request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
293timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
294assert timeout == DEFAULT_TIMEOUT
295
296# explicitly passing the default timeout currently results in it being ignored
297with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
298client = OpenAI(
299base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
300)
301
302request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
303timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
304assert timeout == DEFAULT_TIMEOUT # our default
305
dae0ec80Stainless Bot2 years ago306async def test_invalid_http_client(self) -> None:
307with pytest.raises(TypeError, match="Invalid `http_client` arg"):
308async with httpx.AsyncClient() as http_client:
309OpenAI(
310base_url=base_url,
311api_key=api_key,
312_strict_response_validation=True,
313http_client=cast(Any, http_client),
314)
315
08b8179aDavid Schnurr2 years ago316def test_default_headers_option(self) -> None:
317client = OpenAI(
318base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
319)
320request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
321assert request.headers.get("x-foo") == "bar"
322assert request.headers.get("x-stainless-lang") == "python"
323
324client2 = OpenAI(
325base_url=base_url,
326api_key=api_key,
327_strict_response_validation=True,
328default_headers={
329"X-Foo": "stainless",
330"X-Stainless-Lang": "my-overriding-header",
331},
332)
333request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
334assert request.headers.get("x-foo") == "stainless"
335assert request.headers.get("x-stainless-lang") == "my-overriding-header"
336
337def test_validate_headers(self) -> None:
338client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
339request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
340assert request.headers.get("Authorization") == f"Bearer {api_key}"
341
e967f5a5Stainless Bot2 years ago342with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago343with update_env(**{"OPENAI_API_KEY": Omit()}):
344client2 = OpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago345_ = client2
346
347def test_default_query_option(self) -> None:
348client = OpenAI(
349base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
350)
351request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
352url = httpx.URL(request.url)
353assert dict(url.params) == {"query_param": "bar"}
354
355request = client._build_request(
356FinalRequestOptions(
357method="get",
358url="/foo",
eba67815stainless-app[bot]1 years ago359params={"foo": "baz", "query_param": "overridden"},
08b8179aDavid Schnurr2 years ago360)
361)
362url = httpx.URL(request.url)
eba67815stainless-app[bot]1 years ago363assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
08b8179aDavid Schnurr2 years ago364
365def test_request_extra_json(self) -> None:
366request = self.client._build_request(
367FinalRequestOptions(
368method="post",
369url="/foo",
370json_data={"foo": "bar"},
371extra_json={"baz": False},
372),
373)
374data = json.loads(request.content.decode("utf-8"))
375assert data == {"foo": "bar", "baz": False}
376
377request = self.client._build_request(
378FinalRequestOptions(
379method="post",
380url="/foo",
381extra_json={"baz": False},
382),
383)
384data = json.loads(request.content.decode("utf-8"))
385assert data == {"baz": False}
386
387# `extra_json` takes priority over `json_data` when keys clash
388request = self.client._build_request(
389FinalRequestOptions(
390method="post",
391url="/foo",
392json_data={"foo": "bar", "baz": True},
393extra_json={"baz": None},
394),
395)
396data = json.loads(request.content.decode("utf-8"))
397assert data == {"foo": "bar", "baz": None}
398
399def test_request_extra_headers(self) -> None:
400request = self.client._build_request(
401FinalRequestOptions(
402method="post",
403url="/foo",
404**make_request_options(extra_headers={"X-Foo": "Foo"}),
405),
406)
407assert request.headers.get("X-Foo") == "Foo"
408
409# `extra_headers` takes priority over `default_headers` when keys clash
410request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
411FinalRequestOptions(
412method="post",
413url="/foo",
414**make_request_options(
415extra_headers={"X-Bar": "false"},
416),
417),
418)
419assert request.headers.get("X-Bar") == "false"
420
421def test_request_extra_query(self) -> None:
422request = self.client._build_request(
423FinalRequestOptions(
424method="post",
425url="/foo",
426**make_request_options(
427extra_query={"my_query_param": "Foo"},
428),
429),
430)
31573844Stainless Bot2 years ago431params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago432assert params == {"my_query_param": "Foo"}
433
434# if both `query` and `extra_query` are given, they are merged
435request = self.client._build_request(
436FinalRequestOptions(
437method="post",
438url="/foo",
439**make_request_options(
440query={"bar": "1"},
441extra_query={"foo": "2"},
442),
443),
444)
31573844Stainless Bot2 years ago445params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago446assert params == {"bar": "1", "foo": "2"}
447
448# `extra_query` takes priority over `query` when keys clash
449request = self.client._build_request(
450FinalRequestOptions(
451method="post",
452url="/foo",
453**make_request_options(
454query={"foo": "1"},
455extra_query={"foo": "2"},
456),
457),
458)
31573844Stainless Bot2 years ago459params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago460assert params == {"foo": "2"}
461
22713fd0Stainless Bot2 years ago462def test_multipart_repeating_array(self, client: OpenAI) -> None:
463request = client._build_request(
464FinalRequestOptions.construct(
465method="get",
466url="/foo",
467headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
468json_data={"array": ["foo", "bar"]},
469files=[("foo.txt", b"hello world")],
470)
471)
472
473assert request.read().split(b"\r\n") == [
474b"--6b7ba517decee4a450543ea6ae821c82",
475b'Content-Disposition: form-data; name="array[]"',
476b"",
477b"foo",
478b"--6b7ba517decee4a450543ea6ae821c82",
479b'Content-Disposition: form-data; name="array[]"',
480b"",
481b"bar",
482b"--6b7ba517decee4a450543ea6ae821c82",
483b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
484b"Content-Type: application/octet-stream",
485b"",
486b"hello world",
487b"--6b7ba517decee4a450543ea6ae821c82--",
488b"",
489]
490
08b8179aDavid Schnurr2 years ago491@pytest.mark.respx(base_url=base_url)
492def test_basic_union_response(self, respx_mock: MockRouter) -> None:
493class Model1(BaseModel):
494name: str
495
496class Model2(BaseModel):
497foo: str
498
499respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
500
501response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
502assert isinstance(response, Model2)
503assert response.foo == "bar"
504
505@pytest.mark.respx(base_url=base_url)
506def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
507"""Union of objects with the same field name using a different type"""
508
509class Model1(BaseModel):
510foo: int
511
512class Model2(BaseModel):
513foo: str
514
515respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
516
517response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
518assert isinstance(response, Model2)
519assert response.foo == "bar"
520
521respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
522
523response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
524assert isinstance(response, Model1)
525assert response.foo == 1
526
c26014e2Stainless Bot2 years ago527@pytest.mark.respx(base_url=base_url)
528def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
529"""
530Response that sets Content-Type to something other than application/json but returns json data
531"""
532
533class Model(BaseModel):
534foo: int
535
536respx_mock.get("/foo").mock(
537return_value=httpx.Response(
538200,
539content=json.dumps({"foo": 2}),
540headers={"Content-Type": "application/text"},
541)
542)
543
544response = self.client.get("/foo", cast_to=Model)
545assert isinstance(response, Model)
546assert response.foo == 2
547
f6f38a9bStainless Bot2 years ago548def test_base_url_setter(self) -> None:
549client = OpenAI(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True)
550assert client.base_url == "https://example.com/from_init/"
551
552client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
553
554assert client.base_url == "https://example.com/from_setter/"
555
0733934fStainless Bot2 years ago556def test_base_url_env(self) -> None:
557with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
558client = OpenAI(api_key=api_key, _strict_response_validation=True)
559assert client.base_url == "http://localhost:5000/from/env/"
560
08b8179aDavid Schnurr2 years ago561@pytest.mark.parametrize(
562"client",
563[
564OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
565OpenAI(
566base_url="http://localhost:5000/custom/path/",
567api_key=api_key,
568_strict_response_validation=True,
569http_client=httpx.Client(),
570),
571],
572ids=["standard", "custom http client"],
573)
574def test_base_url_trailing_slash(self, client: OpenAI) -> None:
575request = client._build_request(
576FinalRequestOptions(
577method="post",
578url="/foo",
579json_data={"foo": "bar"},
580),
581)
582assert request.url == "http://localhost:5000/custom/path/foo"
583
584@pytest.mark.parametrize(
585"client",
586[
587OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
588OpenAI(
589base_url="http://localhost:5000/custom/path/",
590api_key=api_key,
591_strict_response_validation=True,
592http_client=httpx.Client(),
593),
594],
595ids=["standard", "custom http client"],
596)
597def test_base_url_no_trailing_slash(self, client: OpenAI) -> None:
598request = client._build_request(
599FinalRequestOptions(
600method="post",
601url="/foo",
602json_data={"foo": "bar"},
603),
604)
605assert request.url == "http://localhost:5000/custom/path/foo"
606
607@pytest.mark.parametrize(
608"client",
609[
610OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
611OpenAI(
612base_url="http://localhost:5000/custom/path/",
613api_key=api_key,
614_strict_response_validation=True,
615http_client=httpx.Client(),
616),
617],
618ids=["standard", "custom http client"],
619)
620def test_absolute_request_url(self, client: OpenAI) -> None:
621request = client._build_request(
622FinalRequestOptions(
623method="post",
624url="https://myapi.com/foo",
625json_data={"foo": "bar"},
626),
627)
628assert request.url == "https://myapi.com/foo"
629
630def test_copied_client_does_not_close_http(self) -> None:
631client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
632assert not client.is_closed()
633
634copied = client.copy()
635assert copied is not client
636
a7ebc260Stainless Bot2 years ago637del copied
08b8179aDavid Schnurr2 years ago638
639assert not client.is_closed()
640
641def test_client_context_manager(self) -> None:
642client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
643with client as c2:
644assert c2 is client
645assert not c2.is_closed()
646assert not client.is_closed()
647assert client.is_closed()
648
649@pytest.mark.respx(base_url=base_url)
650def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
651class Model(BaseModel):
652foo: str
653
654respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
655
656with pytest.raises(APIResponseValidationError) as exc:
657self.client.get("/foo", cast_to=Model)
658
659assert isinstance(exc.value.__cause__, ValidationError)
660
07079085Stainless Bot2 years ago661def test_client_max_retries_validation(self) -> None:
662with pytest.raises(TypeError, match=r"max_retries cannot be None"):
663OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None))
664
08b8179aDavid Schnurr2 years ago665@pytest.mark.respx(base_url=base_url)
666def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
667class Model(BaseModel):
668name: str
669
670respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
671
86379b44Stainless Bot2 years ago672stream = self.client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model])
673assert isinstance(stream, Stream)
674stream.response.close()
08b8179aDavid Schnurr2 years ago675
676@pytest.mark.respx(base_url=base_url)
677def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
678class Model(BaseModel):
679name: str
680
681respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
682
683strict_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
684
685with pytest.raises(APIResponseValidationError):
686strict_client.get("/foo", cast_to=Model)
687
688client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
689
690response = client.get("/foo", cast_to=Model)
691assert isinstance(response, str) # type: ignore[unreachable]
692
693@pytest.mark.parametrize(
694"remaining_retries,retry_after,timeout",
695[
696[3, "20", 20],
697[3, "0", 0.5],
698[3, "-10", 0.5],
699[3, "60", 60],
700[3, "61", 0.5],
701[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
702[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
703[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
704[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
705[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
706[3, "99999999999999999999999999999999999", 0.5],
707[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
708[3, "", 0.5],
709[2, "", 0.5 * 2.0],
710[1, "", 0.5 * 4.0],
7f6a921cstainless-app[bot]1 years ago711[-1100, "", 8], # test large number potentially overflowing
08b8179aDavid Schnurr2 years ago712],
713)
714@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
715def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
716client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
717
718headers = httpx.Headers({"retry-after": retry_after})
719options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
720calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
721assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
722
ba4f7a97Stainless Bot2 years ago723@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
724@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago725def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: OpenAI) -> None:
ba4f7a97Stainless Bot2 years ago726respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
727
728with pytest.raises(APITimeoutError):
0bef1d02stainless-app[bot]1 years ago729client.chat.completions.with_streaming_response.create(
730messages=[
731{
732"content": "string",
733"role": "developer",
734}
735],
736model="gpt-4o",
737).__enter__()
7aad3405Stainless Bot2 years ago738
739assert _get_open_connections(self.client) == 0
740
ba4f7a97Stainless Bot2 years ago741@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago742@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago743def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: OpenAI) -> None:
ba4f7a97Stainless Bot2 years ago744respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago745
ba4f7a97Stainless Bot2 years ago746with pytest.raises(APIStatusError):
0bef1d02stainless-app[bot]1 years ago747client.chat.completions.with_streaming_response.create(
748messages=[
749{
750"content": "string",
751"role": "developer",
752}
753],
754model="gpt-4o",
755).__enter__()
ba4f7a97Stainless Bot2 years ago756assert _get_open_connections(self.client) == 0
7aad3405Stainless Bot2 years ago757
98d8b2acstainless-app[bot]1 years ago758@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
759@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
760@pytest.mark.respx(base_url=base_url)
6634f525stainless-app[bot]1 years ago761@pytest.mark.parametrize("failure_mode", ["status", "exception"])
762def test_retries_taken(
763self,
764client: OpenAI,
765failures_before_success: int,
766failure_mode: Literal["status", "exception"],
767respx_mock: MockRouter,
768) -> None:
98d8b2acstainless-app[bot]1 years ago769client = client.with_options(max_retries=4)
770
771nb_retries = 0
772
773def retry_handler(_request: httpx.Request) -> httpx.Response:
774nonlocal nb_retries
775if nb_retries < failures_before_success:
776nb_retries += 1
6634f525stainless-app[bot]1 years ago777if failure_mode == "exception":
778raise RuntimeError("oops")
98d8b2acstainless-app[bot]1 years ago779return httpx.Response(500)
780return httpx.Response(200)
781
782respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
783
784response = client.chat.completions.with_raw_response.create(
785messages=[
786{
bf1ca86cRobert Craigie1 years ago787"content": "string",
575ff607stainless-app[bot]1 years ago788"role": "developer",
98d8b2acstainless-app[bot]1 years ago789}
790],
bf1ca86cRobert Craigie1 years ago791model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago792)
793
794assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago795assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
796
797@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
798@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
799@pytest.mark.respx(base_url=base_url)
800def test_omit_retry_count_header(
801self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
802) -> None:
803client = client.with_options(max_retries=4)
804
805nb_retries = 0
806
807def retry_handler(_request: httpx.Request) -> httpx.Response:
808nonlocal nb_retries
809if nb_retries < failures_before_success:
810nb_retries += 1
811return httpx.Response(500)
812return httpx.Response(200)
813
814respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
815
816response = client.chat.completions.with_raw_response.create(
817messages=[
818{
819"content": "string",
575ff607stainless-app[bot]1 years ago820"role": "developer",
5449e208Stainless Bot1 years ago821}
822],
823model="gpt-4o",
824extra_headers={"x-stainless-retry-count": Omit()},
825)
826
827assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
828
829@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
830@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
831@pytest.mark.respx(base_url=base_url)
832def test_overwrite_retry_count_header(
833self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
834) -> None:
835client = client.with_options(max_retries=4)
836
837nb_retries = 0
838
839def retry_handler(_request: httpx.Request) -> httpx.Response:
840nonlocal nb_retries
841if nb_retries < failures_before_success:
842nb_retries += 1
843return httpx.Response(500)
844return httpx.Response(200)
845
846respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
847
848response = client.chat.completions.with_raw_response.create(
849messages=[
850{
851"content": "string",
575ff607stainless-app[bot]1 years ago852"role": "developer",
5449e208Stainless Bot1 years ago853}
854],
855model="gpt-4o",
856extra_headers={"x-stainless-retry-count": "42"},
857)
858
859assert response.http_request.headers.get("x-stainless-retry-count") == "42"
98d8b2acstainless-app[bot]1 years ago860
861@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
862@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
863@pytest.mark.respx(base_url=base_url)
864def test_retries_taken_new_response_class(
865self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
866) -> None:
867client = client.with_options(max_retries=4)
868
869nb_retries = 0
870
871def retry_handler(_request: httpx.Request) -> httpx.Response:
872nonlocal nb_retries
873if nb_retries < failures_before_success:
874nb_retries += 1
875return httpx.Response(500)
876return httpx.Response(200)
877
878respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
879
880with client.chat.completions.with_streaming_response.create(
881messages=[
882{
bf1ca86cRobert Craigie1 years ago883"content": "string",
575ff607stainless-app[bot]1 years ago884"role": "developer",
98d8b2acstainless-app[bot]1 years ago885}
886],
bf1ca86cRobert Craigie1 years ago887model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago888) as response:
889assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago890assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
98d8b2acstainless-app[bot]1 years ago891
cc2c1fc1stainless-app[bot]1 years ago892def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
893# Test that the proxy environment variables are set correctly
894monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
895
896client = DefaultHttpxClient()
897
898mounts = tuple(client._mounts.items())
899assert len(mounts) == 1
900assert mounts[0][0].pattern == "https://"
901
902@pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
903def test_default_client_creation(self) -> None:
904# Ensure that the client can be initialized without any exceptions
905DefaultHttpxClient(
906verify=True,
907cert=None,
908trust_env=True,
909http1=True,
910http2=False,
911limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
912)
913
cca09707stainless-app[bot]1 years ago914@pytest.mark.respx(base_url=base_url)
915def test_follow_redirects(self, respx_mock: MockRouter) -> None:
916# Test that the default follow_redirects=True allows following redirects
917respx_mock.post("/redirect").mock(
918return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
919)
920respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
921
922response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
923assert response.status_code == 200
924assert response.json() == {"status": "ok"}
925
926@pytest.mark.respx(base_url=base_url)
927def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
928# Test that follow_redirects=False prevents following redirects
929respx_mock.post("/redirect").mock(
930return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
931)
932
933with pytest.raises(APIStatusError) as exc_info:
934self.client.post(
935"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
936)
937
938assert exc_info.value.response.status_code == 302
939assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
940
08b8179aDavid Schnurr2 years ago941
942class TestAsyncOpenAI:
943client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
944
945@pytest.mark.respx(base_url=base_url)
946@pytest.mark.asyncio
947async def test_raw_response(self, respx_mock: MockRouter) -> None:
c5975bd0Stainless Bot2 years ago948respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago949
950response = await self.client.post("/foo", cast_to=httpx.Response)
951assert response.status_code == 200
952assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago953assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago954
955@pytest.mark.respx(base_url=base_url)
956@pytest.mark.asyncio
957async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
958respx_mock.post("/foo").mock(
959return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
960)
961
962response = await self.client.post("/foo", cast_to=httpx.Response)
963assert response.status_code == 200
964assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago965assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago966
967def test_copy(self) -> None:
968copied = self.client.copy()
969assert id(copied) != id(self.client)
970
971copied = self.client.copy(api_key="another My API Key")
972assert copied.api_key == "another My API Key"
973assert self.client.api_key == "My API Key"
974
975def test_copy_default_options(self) -> None:
976# options that have a default are overridden correctly
977copied = self.client.copy(max_retries=7)
978assert copied.max_retries == 7
979assert self.client.max_retries == 2
980
981copied2 = copied.copy(max_retries=6)
982assert copied2.max_retries == 6
983assert copied.max_retries == 7
984
985# timeout
986assert isinstance(self.client.timeout, httpx.Timeout)
987copied = self.client.copy(timeout=None)
988assert copied.timeout is None
989assert isinstance(self.client.timeout, httpx.Timeout)
990
991def test_copy_default_headers(self) -> None:
992client = AsyncOpenAI(
993base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
994)
995assert client.default_headers["X-Foo"] == "bar"
996
997# does not override the already given value when not specified
998copied = client.copy()
999assert copied.default_headers["X-Foo"] == "bar"
1000
1001# merges already given headers
1002copied = client.copy(default_headers={"X-Bar": "stainless"})
1003assert copied.default_headers["X-Foo"] == "bar"
1004assert copied.default_headers["X-Bar"] == "stainless"
1005
1006# uses new values for any already given headers
1007copied = client.copy(default_headers={"X-Foo": "stainless"})
1008assert copied.default_headers["X-Foo"] == "stainless"
1009
1010# set_default_headers
1011
1012# completely overrides already set values
1013copied = client.copy(set_default_headers={})
1014assert copied.default_headers.get("X-Foo") is None
1015
1016copied = client.copy(set_default_headers={"X-Bar": "Robert"})
1017assert copied.default_headers["X-Bar"] == "Robert"
1018
1019with pytest.raises(
1020ValueError,
1021match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
1022):
1023client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
1024
1025def test_copy_default_query(self) -> None:
1026client = AsyncOpenAI(
1027base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
1028)
1029assert _get_params(client)["foo"] == "bar"
1030
1031# does not override the already given value when not specified
1032copied = client.copy()
1033assert _get_params(copied)["foo"] == "bar"
1034
1035# merges already given params
1036copied = client.copy(default_query={"bar": "stainless"})
1037params = _get_params(copied)
1038assert params["foo"] == "bar"
1039assert params["bar"] == "stainless"
1040
1041# uses new values for any already given headers
1042copied = client.copy(default_query={"foo": "stainless"})
1043assert _get_params(copied)["foo"] == "stainless"
1044
1045# set_default_query
1046
1047# completely overrides already set values
1048copied = client.copy(set_default_query={})
1049assert _get_params(copied) == {}
1050
1051copied = client.copy(set_default_query={"bar": "Robert"})
1052assert _get_params(copied)["bar"] == "Robert"
1053
1054with pytest.raises(
1055ValueError,
1056# TODO: update
1057match="`default_query` and `set_default_query` arguments are mutually exclusive",
1058):
1059client.copy(set_default_query={}, default_query={"foo": "Bar"})
1060
1061def test_copy_signature(self) -> None:
1062# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
1063init_signature = inspect.signature(
1064# mypy doesn't like that we access the `__init__` property.
1065self.client.__init__, # type: ignore[misc]
1066)
1067copy_signature = inspect.signature(self.client.copy)
1068exclude_params = {"transport", "proxies", "_strict_response_validation"}
1069
1070for name in init_signature.parameters.keys():
1071if name in exclude_params:
1072continue
1073
1074copy_param = copy_signature.parameters.get(name)
1075assert copy_param is not None, f"copy() signature is missing the {name} param"
1076
d052708aStainless Bot2 years ago1077def test_copy_build_request(self) -> None:
1078options = FinalRequestOptions(method="get", url="/foo")
1079
1080def build_request(options: FinalRequestOptions) -> None:
1081client = self.client.copy()
1082client._build_request(options)
1083
1084# ensure that the machinery is warmed up before tracing starts.
1085build_request(options)
1086gc.collect()
1087
1088tracemalloc.start(1000)
1089
1090snapshot_before = tracemalloc.take_snapshot()
1091
1092ITERATIONS = 10
1093for _ in range(ITERATIONS):
1094build_request(options)
1095
ce04ec28Stainless Bot2 years ago1096gc.collect()
d052708aStainless Bot2 years ago1097snapshot_after = tracemalloc.take_snapshot()
1098
1099tracemalloc.stop()
1100
1101def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
1102if diff.count == 0:
1103# Avoid false positives by considering only leaks (i.e. allocations that persist).
1104return
1105
1106if diff.count % ITERATIONS != 0:
1107# Avoid false positives by considering only leaks that appear per iteration.
1108return
1109
1110for frame in diff.traceback:
1111if any(
1112frame.filename.endswith(fragment)
1113for fragment in [
1114# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
1115#
1116# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago1117"openai/_legacy_response.py",
d052708aStainless Bot2 years ago1118"openai/_response.py",
1119# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
1120"openai/_compat.py",
1121# Standard library leaks we don't care about.
1122"/logging/__init__.py",
1123]
1124):
1125return
1126
1127leaks.append(diff)
1128
1129leaks: list[tracemalloc.StatisticDiff] = []
1130for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
1131add_leak(leaks, diff)
1132if leaks:
1133for leak in leaks:
1134print("MEMORY LEAK:", leak)
1135for frame in leak.traceback:
1136print(frame)
1137raise AssertionError()
1138
08b8179aDavid Schnurr2 years ago1139async def test_request_timeout(self) -> None:
1140request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
1141timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1142assert timeout == DEFAULT_TIMEOUT
1143
1144request = self.client._build_request(
1145FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
1146)
1147timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1148assert timeout == httpx.Timeout(100.0)
1149
1150async def test_client_timeout_option(self) -> None:
1151client = AsyncOpenAI(
1152base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0)
1153)
1154
1155request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1156timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1157assert timeout == httpx.Timeout(0)
1158
1159async def test_http_client_timeout_option(self) -> None:
1160# custom timeout given to the httpx client should be used
1161async with httpx.AsyncClient(timeout=None) as http_client:
1162client = AsyncOpenAI(
1163base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1164)
1165
1166request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1167timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1168assert timeout == httpx.Timeout(None)
1169
1170# no timeout given to the httpx client should not use the httpx default
1171async with httpx.AsyncClient() as http_client:
1172client = AsyncOpenAI(
1173base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1174)
1175
1176request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1177timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1178assert timeout == DEFAULT_TIMEOUT
1179
1180# explicitly passing the default timeout currently results in it being ignored
1181async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
1182client = AsyncOpenAI(
1183base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1184)
1185
1186request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1187timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1188assert timeout == DEFAULT_TIMEOUT # our default
1189
dae0ec80Stainless Bot2 years ago1190def test_invalid_http_client(self) -> None:
1191with pytest.raises(TypeError, match="Invalid `http_client` arg"):
1192with httpx.Client() as http_client:
1193AsyncOpenAI(
1194base_url=base_url,
1195api_key=api_key,
1196_strict_response_validation=True,
1197http_client=cast(Any, http_client),
1198)
1199
08b8179aDavid Schnurr2 years ago1200def test_default_headers_option(self) -> None:
1201client = AsyncOpenAI(
1202base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
1203)
1204request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1205assert request.headers.get("x-foo") == "bar"
1206assert request.headers.get("x-stainless-lang") == "python"
1207
1208client2 = AsyncOpenAI(
1209base_url=base_url,
1210api_key=api_key,
1211_strict_response_validation=True,
1212default_headers={
1213"X-Foo": "stainless",
1214"X-Stainless-Lang": "my-overriding-header",
1215},
1216)
1217request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
1218assert request.headers.get("x-foo") == "stainless"
1219assert request.headers.get("x-stainless-lang") == "my-overriding-header"
1220
1221def test_validate_headers(self) -> None:
1222client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1223request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1224assert request.headers.get("Authorization") == f"Bearer {api_key}"
1225
e967f5a5Stainless Bot2 years ago1226with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago1227with update_env(**{"OPENAI_API_KEY": Omit()}):
1228client2 = AsyncOpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago1229_ = client2
1230
1231def test_default_query_option(self) -> None:
1232client = AsyncOpenAI(
1233base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
1234)
1235request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1236url = httpx.URL(request.url)
1237assert dict(url.params) == {"query_param": "bar"}
1238
1239request = client._build_request(
1240FinalRequestOptions(
1241method="get",
1242url="/foo",
eba67815stainless-app[bot]1 years ago1243params={"foo": "baz", "query_param": "overridden"},
08b8179aDavid Schnurr2 years ago1244)
1245)
1246url = httpx.URL(request.url)
eba67815stainless-app[bot]1 years ago1247assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
08b8179aDavid Schnurr2 years ago1248
1249def test_request_extra_json(self) -> None:
1250request = self.client._build_request(
1251FinalRequestOptions(
1252method="post",
1253url="/foo",
1254json_data={"foo": "bar"},
1255extra_json={"baz": False},
1256),
1257)
1258data = json.loads(request.content.decode("utf-8"))
1259assert data == {"foo": "bar", "baz": False}
1260
1261request = self.client._build_request(
1262FinalRequestOptions(
1263method="post",
1264url="/foo",
1265extra_json={"baz": False},
1266),
1267)
1268data = json.loads(request.content.decode("utf-8"))
1269assert data == {"baz": False}
1270
1271# `extra_json` takes priority over `json_data` when keys clash
1272request = self.client._build_request(
1273FinalRequestOptions(
1274method="post",
1275url="/foo",
1276json_data={"foo": "bar", "baz": True},
1277extra_json={"baz": None},
1278),
1279)
1280data = json.loads(request.content.decode("utf-8"))
1281assert data == {"foo": "bar", "baz": None}
1282
1283def test_request_extra_headers(self) -> None:
1284request = self.client._build_request(
1285FinalRequestOptions(
1286method="post",
1287url="/foo",
1288**make_request_options(extra_headers={"X-Foo": "Foo"}),
1289),
1290)
1291assert request.headers.get("X-Foo") == "Foo"
1292
1293# `extra_headers` takes priority over `default_headers` when keys clash
1294request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
1295FinalRequestOptions(
1296method="post",
1297url="/foo",
1298**make_request_options(
1299extra_headers={"X-Bar": "false"},
1300),
1301),
1302)
1303assert request.headers.get("X-Bar") == "false"
1304
1305def test_request_extra_query(self) -> None:
1306request = self.client._build_request(
1307FinalRequestOptions(
1308method="post",
1309url="/foo",
1310**make_request_options(
1311extra_query={"my_query_param": "Foo"},
1312),
1313),
1314)
31573844Stainless Bot2 years ago1315params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1316assert params == {"my_query_param": "Foo"}
1317
1318# if both `query` and `extra_query` are given, they are merged
1319request = self.client._build_request(
1320FinalRequestOptions(
1321method="post",
1322url="/foo",
1323**make_request_options(
1324query={"bar": "1"},
1325extra_query={"foo": "2"},
1326),
1327),
1328)
31573844Stainless Bot2 years ago1329params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1330assert params == {"bar": "1", "foo": "2"}
1331
1332# `extra_query` takes priority over `query` when keys clash
1333request = self.client._build_request(
1334FinalRequestOptions(
1335method="post",
1336url="/foo",
1337**make_request_options(
1338query={"foo": "1"},
1339extra_query={"foo": "2"},
1340),
1341),
1342)
31573844Stainless Bot2 years ago1343params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1344assert params == {"foo": "2"}
1345
22713fd0Stainless Bot2 years ago1346def test_multipart_repeating_array(self, async_client: AsyncOpenAI) -> None:
1347request = async_client._build_request(
1348FinalRequestOptions.construct(
1349method="get",
1350url="/foo",
1351headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
1352json_data={"array": ["foo", "bar"]},
1353files=[("foo.txt", b"hello world")],
1354)
1355)
1356
1357assert request.read().split(b"\r\n") == [
1358b"--6b7ba517decee4a450543ea6ae821c82",
1359b'Content-Disposition: form-data; name="array[]"',
1360b"",
1361b"foo",
1362b"--6b7ba517decee4a450543ea6ae821c82",
1363b'Content-Disposition: form-data; name="array[]"',
1364b"",
1365b"bar",
1366b"--6b7ba517decee4a450543ea6ae821c82",
1367b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
1368b"Content-Type: application/octet-stream",
1369b"",
1370b"hello world",
1371b"--6b7ba517decee4a450543ea6ae821c82--",
1372b"",
1373]
1374
08b8179aDavid Schnurr2 years ago1375@pytest.mark.respx(base_url=base_url)
1376async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
1377class Model1(BaseModel):
1378name: str
1379
1380class Model2(BaseModel):
1381foo: str
1382
1383respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1384
1385response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1386assert isinstance(response, Model2)
1387assert response.foo == "bar"
1388
1389@pytest.mark.respx(base_url=base_url)
1390async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
1391"""Union of objects with the same field name using a different type"""
1392
1393class Model1(BaseModel):
1394foo: int
1395
1396class Model2(BaseModel):
1397foo: str
1398
1399respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1400
1401response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1402assert isinstance(response, Model2)
1403assert response.foo == "bar"
1404
1405respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
1406
1407response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1408assert isinstance(response, Model1)
1409assert response.foo == 1
1410
c26014e2Stainless Bot2 years ago1411@pytest.mark.respx(base_url=base_url)
1412async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
1413"""
1414Response that sets Content-Type to something other than application/json but returns json data
1415"""
1416
1417class Model(BaseModel):
1418foo: int
1419
1420respx_mock.get("/foo").mock(
1421return_value=httpx.Response(
1422200,
1423content=json.dumps({"foo": 2}),
1424headers={"Content-Type": "application/text"},
1425)
1426)
1427
1428response = await self.client.get("/foo", cast_to=Model)
1429assert isinstance(response, Model)
1430assert response.foo == 2
1431
f6f38a9bStainless Bot2 years ago1432def test_base_url_setter(self) -> None:
1433client = AsyncOpenAI(
1434base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True
1435)
1436assert client.base_url == "https://example.com/from_init/"
1437
1438client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
1439
1440assert client.base_url == "https://example.com/from_setter/"
1441
0733934fStainless Bot2 years ago1442def test_base_url_env(self) -> None:
1443with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
1444client = AsyncOpenAI(api_key=api_key, _strict_response_validation=True)
1445assert client.base_url == "http://localhost:5000/from/env/"
1446
08b8179aDavid Schnurr2 years ago1447@pytest.mark.parametrize(
1448"client",
1449[
1450AsyncOpenAI(
1451base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1452),
1453AsyncOpenAI(
1454base_url="http://localhost:5000/custom/path/",
1455api_key=api_key,
1456_strict_response_validation=True,
1457http_client=httpx.AsyncClient(),
1458),
1459],
1460ids=["standard", "custom http client"],
1461)
1462def test_base_url_trailing_slash(self, client: AsyncOpenAI) -> None:
1463request = client._build_request(
1464FinalRequestOptions(
1465method="post",
1466url="/foo",
1467json_data={"foo": "bar"},
1468),
1469)
1470assert request.url == "http://localhost:5000/custom/path/foo"
1471
1472@pytest.mark.parametrize(
1473"client",
1474[
1475AsyncOpenAI(
1476base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1477),
1478AsyncOpenAI(
1479base_url="http://localhost:5000/custom/path/",
1480api_key=api_key,
1481_strict_response_validation=True,
1482http_client=httpx.AsyncClient(),
1483),
1484],
1485ids=["standard", "custom http client"],
1486)
1487def test_base_url_no_trailing_slash(self, client: AsyncOpenAI) -> None:
1488request = client._build_request(
1489FinalRequestOptions(
1490method="post",
1491url="/foo",
1492json_data={"foo": "bar"},
1493),
1494)
1495assert request.url == "http://localhost:5000/custom/path/foo"
1496
1497@pytest.mark.parametrize(
1498"client",
1499[
1500AsyncOpenAI(
1501base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1502),
1503AsyncOpenAI(
1504base_url="http://localhost:5000/custom/path/",
1505api_key=api_key,
1506_strict_response_validation=True,
1507http_client=httpx.AsyncClient(),
1508),
1509],
1510ids=["standard", "custom http client"],
1511)
1512def test_absolute_request_url(self, client: AsyncOpenAI) -> None:
1513request = client._build_request(
1514FinalRequestOptions(
1515method="post",
1516url="https://myapi.com/foo",
1517json_data={"foo": "bar"},
1518),
1519)
1520assert request.url == "https://myapi.com/foo"
1521
1522async def test_copied_client_does_not_close_http(self) -> None:
1523client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1524assert not client.is_closed()
1525
1526copied = client.copy()
1527assert copied is not client
1528
a7ebc260Stainless Bot2 years ago1529del copied
08b8179aDavid Schnurr2 years ago1530
1531await asyncio.sleep(0.2)
1532assert not client.is_closed()
1533
1534async def test_client_context_manager(self) -> None:
1535client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1536async with client as c2:
1537assert c2 is client
1538assert not c2.is_closed()
1539assert not client.is_closed()
1540assert client.is_closed()
1541
1542@pytest.mark.respx(base_url=base_url)
1543@pytest.mark.asyncio
1544async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
1545class Model(BaseModel):
1546foo: str
1547
1548respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
1549
1550with pytest.raises(APIResponseValidationError) as exc:
1551await self.client.get("/foo", cast_to=Model)
1552
1553assert isinstance(exc.value.__cause__, ValidationError)
1554
07079085Stainless Bot2 years ago1555async def test_client_max_retries_validation(self) -> None:
1556with pytest.raises(TypeError, match=r"max_retries cannot be None"):
1557AsyncOpenAI(
1558base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)
1559)
1560
08b8179aDavid Schnurr2 years ago1561@pytest.mark.respx(base_url=base_url)
1562@pytest.mark.asyncio
1563async def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
1564class Model(BaseModel):
1565name: str
1566
1567respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1568
86379b44Stainless Bot2 years ago1569stream = await self.client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
1570assert isinstance(stream, AsyncStream)
1571await stream.response.aclose()
08b8179aDavid Schnurr2 years ago1572
1573@pytest.mark.respx(base_url=base_url)
1574@pytest.mark.asyncio
1575async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
1576class Model(BaseModel):
1577name: str
1578
1579respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
1580
1581strict_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1582
1583with pytest.raises(APIResponseValidationError):
1584await strict_client.get("/foo", cast_to=Model)
1585
1586client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
1587
1588response = await client.get("/foo", cast_to=Model)
1589assert isinstance(response, str) # type: ignore[unreachable]
1590
1591@pytest.mark.parametrize(
1592"remaining_retries,retry_after,timeout",
1593[
1594[3, "20", 20],
1595[3, "0", 0.5],
1596[3, "-10", 0.5],
1597[3, "60", 60],
1598[3, "61", 0.5],
1599[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
1600[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
1601[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
1602[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
1603[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
1604[3, "99999999999999999999999999999999999", 0.5],
1605[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
1606[3, "", 0.5],
1607[2, "", 0.5 * 2.0],
1608[1, "", 0.5 * 4.0],
7f6a921cstainless-app[bot]1 years ago1609[-1100, "", 8], # test large number potentially overflowing
08b8179aDavid Schnurr2 years ago1610],
1611)
1612@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
1613@pytest.mark.asyncio
1614async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
1615client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1616
1617headers = httpx.Headers({"retry-after": retry_after})
1618options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
1619calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
1620assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
7aad3405Stainless Bot2 years ago1621
ba4f7a97Stainless Bot2 years ago1622@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1623@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago1624async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
ba4f7a97Stainless Bot2 years ago1625respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
1626
1627with pytest.raises(APITimeoutError):
0bef1d02stainless-app[bot]1 years ago1628await async_client.chat.completions.with_streaming_response.create(
1629messages=[
1630{
1631"content": "string",
1632"role": "developer",
1633}
1634],
1635model="gpt-4o",
1636).__aenter__()
7aad3405Stainless Bot2 years ago1637
1638assert _get_open_connections(self.client) == 0
1639
ba4f7a97Stainless Bot2 years ago1640@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago1641@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago1642async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
ba4f7a97Stainless Bot2 years ago1643respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago1644
ba4f7a97Stainless Bot2 years ago1645with pytest.raises(APIStatusError):
0bef1d02stainless-app[bot]1 years ago1646await async_client.chat.completions.with_streaming_response.create(
1647messages=[
1648{
1649"content": "string",
1650"role": "developer",
1651}
1652],
1653model="gpt-4o",
1654).__aenter__()
ba4f7a97Stainless Bot2 years ago1655assert _get_open_connections(self.client) == 0
98d8b2acstainless-app[bot]1 years ago1656
1657@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1658@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1659@pytest.mark.respx(base_url=base_url)
1660@pytest.mark.asyncio
6634f525stainless-app[bot]1 years ago1661@pytest.mark.parametrize("failure_mode", ["status", "exception"])
98d8b2acstainless-app[bot]1 years ago1662async def test_retries_taken(
6634f525stainless-app[bot]1 years ago1663self,
1664async_client: AsyncOpenAI,
1665failures_before_success: int,
1666failure_mode: Literal["status", "exception"],
1667respx_mock: MockRouter,
98d8b2acstainless-app[bot]1 years ago1668) -> None:
1669client = async_client.with_options(max_retries=4)
1670
1671nb_retries = 0
1672
1673def retry_handler(_request: httpx.Request) -> httpx.Response:
1674nonlocal nb_retries
1675if nb_retries < failures_before_success:
1676nb_retries += 1
6634f525stainless-app[bot]1 years ago1677if failure_mode == "exception":
1678raise RuntimeError("oops")
98d8b2acstainless-app[bot]1 years ago1679return httpx.Response(500)
1680return httpx.Response(200)
1681
1682respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1683
1684response = await client.chat.completions.with_raw_response.create(
1685messages=[
1686{
bf1ca86cRobert Craigie1 years ago1687"content": "string",
575ff607stainless-app[bot]1 years ago1688"role": "developer",
98d8b2acstainless-app[bot]1 years ago1689}
1690],
bf1ca86cRobert Craigie1 years ago1691model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago1692)
1693
1694assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago1695assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
1696
1697@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1698@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1699@pytest.mark.respx(base_url=base_url)
1700@pytest.mark.asyncio
1701async def test_omit_retry_count_header(
1702self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1703) -> None:
1704client = async_client.with_options(max_retries=4)
1705
1706nb_retries = 0
1707
1708def retry_handler(_request: httpx.Request) -> httpx.Response:
1709nonlocal nb_retries
1710if nb_retries < failures_before_success:
1711nb_retries += 1
1712return httpx.Response(500)
1713return httpx.Response(200)
1714
1715respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1716
1717response = await client.chat.completions.with_raw_response.create(
1718messages=[
1719{
1720"content": "string",
575ff607stainless-app[bot]1 years ago1721"role": "developer",
5449e208Stainless Bot1 years ago1722}
1723],
1724model="gpt-4o",
1725extra_headers={"x-stainless-retry-count": Omit()},
1726)
1727
1728assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
1729
1730@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1731@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1732@pytest.mark.respx(base_url=base_url)
1733@pytest.mark.asyncio
1734async def test_overwrite_retry_count_header(
1735self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1736) -> None:
1737client = async_client.with_options(max_retries=4)
1738
1739nb_retries = 0
1740
1741def retry_handler(_request: httpx.Request) -> httpx.Response:
1742nonlocal nb_retries
1743if nb_retries < failures_before_success:
1744nb_retries += 1
1745return httpx.Response(500)
1746return httpx.Response(200)
1747
1748respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1749
1750response = await client.chat.completions.with_raw_response.create(
1751messages=[
1752{
1753"content": "string",
575ff607stainless-app[bot]1 years ago1754"role": "developer",
5449e208Stainless Bot1 years ago1755}
1756],
1757model="gpt-4o",
1758extra_headers={"x-stainless-retry-count": "42"},
1759)
1760
1761assert response.http_request.headers.get("x-stainless-retry-count") == "42"
98d8b2acstainless-app[bot]1 years ago1762
1763@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1764@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1765@pytest.mark.respx(base_url=base_url)
1766@pytest.mark.asyncio
1767async def test_retries_taken_new_response_class(
1768self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1769) -> None:
1770client = async_client.with_options(max_retries=4)
1771
1772nb_retries = 0
1773
1774def retry_handler(_request: httpx.Request) -> httpx.Response:
1775nonlocal nb_retries
1776if nb_retries < failures_before_success:
1777nb_retries += 1
1778return httpx.Response(500)
1779return httpx.Response(200)
1780
1781respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1782
1783async with client.chat.completions.with_streaming_response.create(
1784messages=[
1785{
bf1ca86cRobert Craigie1 years ago1786"content": "string",
575ff607stainless-app[bot]1 years ago1787"role": "developer",
98d8b2acstainless-app[bot]1 years ago1788}
1789],
bf1ca86cRobert Craigie1 years ago1790model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago1791) as response:
1792assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago1793assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
d8901d28Seth Gilchrist1 years ago1794
1795def test_get_platform(self) -> None:
53ab0467Stainless Bot1 years ago1796# A previous implementation of asyncify could leave threads unterminated when
1797# used with nest_asyncio.
1798#
d8901d28Seth Gilchrist1 years ago1799# Since nest_asyncio.apply() is global and cannot be un-applied, this
1800# test is run in a separate process to avoid affecting other tests.
53ab0467Stainless Bot1 years ago1801test_code = dedent("""
d8901d28Seth Gilchrist1 years ago1802import asyncio
1803import nest_asyncio
1804import threading
1805
1806from openai._utils import asyncify
fb69e674stainless-app[bot]1 years ago1807from openai._base_client import get_platform
d8901d28Seth Gilchrist1 years ago1808
1809async def test_main() -> None:
1810result = await asyncify(get_platform)()
1811print(result)
1812for thread in threading.enumerate():
1813print(thread.name)
1814
1815nest_asyncio.apply()
1816asyncio.run(test_main())
1817""")
1818with subprocess.Popen(
1819[sys.executable, "-c", test_code],
1820text=True,
1821) as process:
14543c59stainless-app[bot]1 years ago1822timeout = 10 # seconds
1823
1824start_time = time.monotonic()
1825while True:
1826return_code = process.poll()
1827if return_code is not None:
1828if return_code != 0:
1829raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
1830
1831# success
1832break
1833
1834if time.monotonic() - start_time > timeout:
1835process.kill()
1836raise AssertionError("calling get_platform using asyncify resulted in a hung process")
1837
1838time.sleep(0.1)
cca09707stainless-app[bot]1 years ago1839
cc2c1fc1stainless-app[bot]1 years ago1840async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
1841# Test that the proxy environment variables are set correctly
1842monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
1843
1844client = DefaultAsyncHttpxClient()
1845
1846mounts = tuple(client._mounts.items())
1847assert len(mounts) == 1
1848assert mounts[0][0].pattern == "https://"
1849
1850@pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
1851async def test_default_client_creation(self) -> None:
1852# Ensure that the client can be initialized without any exceptions
1853DefaultAsyncHttpxClient(
1854verify=True,
1855cert=None,
1856trust_env=True,
1857http1=True,
1858http2=False,
1859limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
1860)
1861
cca09707stainless-app[bot]1 years ago1862@pytest.mark.respx(base_url=base_url)
1863async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
1864# Test that the default follow_redirects=True allows following redirects
1865respx_mock.post("/redirect").mock(
1866return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1867)
1868respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
1869
1870response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
1871assert response.status_code == 200
1872assert response.json() == {"status": "ok"}
1873
1874@pytest.mark.respx(base_url=base_url)
1875async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
1876# Test that follow_redirects=False prevents following redirects
1877respx_mock.post("/redirect").mock(
1878return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1879)
1880
1881with pytest.raises(APIStatusError) as exc_info:
1882await self.client.post(
1883"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
1884)
1885
1886assert exc_info.value.response.status_code == 302
1887assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"