openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.85.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/test_client.py

1885lines · 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
300f58bbstainless-app[bot]1 years ago26from openai._utils import maybe_transform
08b8179aDavid Schnurr2 years ago27from openai._models import BaseModel, FinalRequestOptions
86379b44Stainless Bot2 years ago28from openai._constants import RAW_RESPONSE_HEADER
08b8179aDavid Schnurr2 years ago29from openai._streaming import Stream, AsyncStream
a47375b7Stainless Bot2 years ago30from openai._exceptions import OpenAIError, APIStatusError, APITimeoutError, APIResponseValidationError
31from openai._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options
300f58bbstainless-app[bot]1 years ago32from openai.types.chat.completion_create_params import CompletionCreateParamsNonStreaming
08b8179aDavid Schnurr2 years ago33
0733934fStainless Bot2 years ago34from .utils import update_env
35
08b8179aDavid Schnurr2 years ago36base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
37api_key = "My API Key"
38
39
40def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]:
41request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
42url = httpx.URL(request.url)
43return dict(url.params)
44
45
ba4f7a97Stainless Bot2 years ago46def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float:
47return 0.1
7aad3405Stainless Bot2 years ago48
49
50def _get_open_connections(client: OpenAI | AsyncOpenAI) -> int:
51transport = client._client._transport
52assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport)
53
54pool = transport._pool
55return len(pool._requests)
56
57
08b8179aDavid Schnurr2 years ago58class TestOpenAI:
59client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
60
61@pytest.mark.respx(base_url=base_url)
62def test_raw_response(self, respx_mock: MockRouter) -> None:
c5975bd0Stainless Bot2 years ago63respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago64
65response = self.client.post("/foo", cast_to=httpx.Response)
66assert response.status_code == 200
67assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago68assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago69
70@pytest.mark.respx(base_url=base_url)
71def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
72respx_mock.post("/foo").mock(
73return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
74)
75
76response = self.client.post("/foo", cast_to=httpx.Response)
77assert response.status_code == 200
78assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago79assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago80
81def test_copy(self) -> None:
82copied = self.client.copy()
83assert id(copied) != id(self.client)
84
85copied = self.client.copy(api_key="another My API Key")
86assert copied.api_key == "another My API Key"
87assert self.client.api_key == "My API Key"
88
89def test_copy_default_options(self) -> None:
90# options that have a default are overridden correctly
91copied = self.client.copy(max_retries=7)
92assert copied.max_retries == 7
93assert self.client.max_retries == 2
94
95copied2 = copied.copy(max_retries=6)
96assert copied2.max_retries == 6
97assert copied.max_retries == 7
98
99# timeout
100assert isinstance(self.client.timeout, httpx.Timeout)
101copied = self.client.copy(timeout=None)
102assert copied.timeout is None
103assert isinstance(self.client.timeout, httpx.Timeout)
104
105def test_copy_default_headers(self) -> None:
106client = OpenAI(
107base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
108)
109assert client.default_headers["X-Foo"] == "bar"
110
111# does not override the already given value when not specified
112copied = client.copy()
113assert copied.default_headers["X-Foo"] == "bar"
114
115# merges already given headers
116copied = client.copy(default_headers={"X-Bar": "stainless"})
117assert copied.default_headers["X-Foo"] == "bar"
118assert copied.default_headers["X-Bar"] == "stainless"
119
120# uses new values for any already given headers
121copied = client.copy(default_headers={"X-Foo": "stainless"})
122assert copied.default_headers["X-Foo"] == "stainless"
123
124# set_default_headers
125
126# completely overrides already set values
127copied = client.copy(set_default_headers={})
128assert copied.default_headers.get("X-Foo") is None
129
130copied = client.copy(set_default_headers={"X-Bar": "Robert"})
131assert copied.default_headers["X-Bar"] == "Robert"
132
133with pytest.raises(
134ValueError,
135match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
136):
137client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
138
139def test_copy_default_query(self) -> None:
140client = OpenAI(
141base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
142)
143assert _get_params(client)["foo"] == "bar"
144
145# does not override the already given value when not specified
146copied = client.copy()
147assert _get_params(copied)["foo"] == "bar"
148
149# merges already given params
150copied = client.copy(default_query={"bar": "stainless"})
151params = _get_params(copied)
152assert params["foo"] == "bar"
153assert params["bar"] == "stainless"
154
155# uses new values for any already given headers
156copied = client.copy(default_query={"foo": "stainless"})
157assert _get_params(copied)["foo"] == "stainless"
158
159# set_default_query
160
161# completely overrides already set values
162copied = client.copy(set_default_query={})
163assert _get_params(copied) == {}
164
165copied = client.copy(set_default_query={"bar": "Robert"})
166assert _get_params(copied)["bar"] == "Robert"
167
168with pytest.raises(
169ValueError,
170# TODO: update
171match="`default_query` and `set_default_query` arguments are mutually exclusive",
172):
173client.copy(set_default_query={}, default_query={"foo": "Bar"})
174
175def test_copy_signature(self) -> None:
176# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
177init_signature = inspect.signature(
178# mypy doesn't like that we access the `__init__` property.
179self.client.__init__, # type: ignore[misc]
180)
181copy_signature = inspect.signature(self.client.copy)
182exclude_params = {"transport", "proxies", "_strict_response_validation"}
183
184for name in init_signature.parameters.keys():
185if name in exclude_params:
186continue
187
188copy_param = copy_signature.parameters.get(name)
189assert copy_param is not None, f"copy() signature is missing the {name} param"
190
d052708aStainless Bot2 years ago191def test_copy_build_request(self) -> None:
192options = FinalRequestOptions(method="get", url="/foo")
193
194def build_request(options: FinalRequestOptions) -> None:
195client = self.client.copy()
196client._build_request(options)
197
198# ensure that the machinery is warmed up before tracing starts.
199build_request(options)
200gc.collect()
201
202tracemalloc.start(1000)
203
204snapshot_before = tracemalloc.take_snapshot()
205
206ITERATIONS = 10
207for _ in range(ITERATIONS):
208build_request(options)
209
ce04ec28Stainless Bot2 years ago210gc.collect()
d052708aStainless Bot2 years ago211snapshot_after = tracemalloc.take_snapshot()
212
213tracemalloc.stop()
214
215def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
216if diff.count == 0:
217# Avoid false positives by considering only leaks (i.e. allocations that persist).
218return
219
220if diff.count % ITERATIONS != 0:
221# Avoid false positives by considering only leaks that appear per iteration.
222return
223
224for frame in diff.traceback:
225if any(
226frame.filename.endswith(fragment)
227for fragment in [
228# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
229#
230# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago231"openai/_legacy_response.py",
d052708aStainless Bot2 years ago232"openai/_response.py",
233# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
234"openai/_compat.py",
235# Standard library leaks we don't care about.
236"/logging/__init__.py",
237]
238):
239return
240
241leaks.append(diff)
242
243leaks: list[tracemalloc.StatisticDiff] = []
244for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
245add_leak(leaks, diff)
246if leaks:
247for leak in leaks:
248print("MEMORY LEAK:", leak)
249for frame in leak.traceback:
250print(frame)
251raise AssertionError()
252
08b8179aDavid Schnurr2 years ago253def test_request_timeout(self) -> None:
254request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
255timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
256assert timeout == DEFAULT_TIMEOUT
257
258request = self.client._build_request(
259FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
260)
261timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
262assert timeout == httpx.Timeout(100.0)
263
264def test_client_timeout_option(self) -> None:
265client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0))
266
267request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
268timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
269assert timeout == httpx.Timeout(0)
270
271def test_http_client_timeout_option(self) -> None:
272# custom timeout given to the httpx client should be used
273with httpx.Client(timeout=None) as http_client:
274client = OpenAI(
275base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
276)
277
278request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
279timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
280assert timeout == httpx.Timeout(None)
281
282# no timeout given to the httpx client should not use the httpx default
283with httpx.Client() as http_client:
284client = OpenAI(
285base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
286)
287
288request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
289timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
290assert timeout == DEFAULT_TIMEOUT
291
292# explicitly passing the default timeout currently results in it being ignored
293with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
294client = OpenAI(
295base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
296)
297
298request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
299timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
300assert timeout == DEFAULT_TIMEOUT # our default
301
dae0ec80Stainless Bot2 years ago302async def test_invalid_http_client(self) -> None:
303with pytest.raises(TypeError, match="Invalid `http_client` arg"):
304async with httpx.AsyncClient() as http_client:
305OpenAI(
306base_url=base_url,
307api_key=api_key,
308_strict_response_validation=True,
309http_client=cast(Any, http_client),
310)
311
08b8179aDavid Schnurr2 years ago312def test_default_headers_option(self) -> None:
313client = OpenAI(
314base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
315)
316request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
317assert request.headers.get("x-foo") == "bar"
318assert request.headers.get("x-stainless-lang") == "python"
319
320client2 = OpenAI(
321base_url=base_url,
322api_key=api_key,
323_strict_response_validation=True,
324default_headers={
325"X-Foo": "stainless",
326"X-Stainless-Lang": "my-overriding-header",
327},
328)
329request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
330assert request.headers.get("x-foo") == "stainless"
331assert request.headers.get("x-stainless-lang") == "my-overriding-header"
332
333def test_validate_headers(self) -> None:
334client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
335request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
336assert request.headers.get("Authorization") == f"Bearer {api_key}"
337
e967f5a5Stainless Bot2 years ago338with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago339with update_env(**{"OPENAI_API_KEY": Omit()}):
340client2 = OpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago341_ = client2
342
343def test_default_query_option(self) -> None:
344client = OpenAI(
345base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
346)
347request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
348url = httpx.URL(request.url)
349assert dict(url.params) == {"query_param": "bar"}
350
351request = client._build_request(
352FinalRequestOptions(
353method="get",
354url="/foo",
eba67815stainless-app[bot]1 years ago355params={"foo": "baz", "query_param": "overridden"},
08b8179aDavid Schnurr2 years ago356)
357)
358url = httpx.URL(request.url)
eba67815stainless-app[bot]1 years ago359assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
08b8179aDavid Schnurr2 years ago360
361def test_request_extra_json(self) -> None:
362request = self.client._build_request(
363FinalRequestOptions(
364method="post",
365url="/foo",
366json_data={"foo": "bar"},
367extra_json={"baz": False},
368),
369)
370data = json.loads(request.content.decode("utf-8"))
371assert data == {"foo": "bar", "baz": False}
372
373request = self.client._build_request(
374FinalRequestOptions(
375method="post",
376url="/foo",
377extra_json={"baz": False},
378),
379)
380data = json.loads(request.content.decode("utf-8"))
381assert data == {"baz": False}
382
383# `extra_json` takes priority over `json_data` when keys clash
384request = self.client._build_request(
385FinalRequestOptions(
386method="post",
387url="/foo",
388json_data={"foo": "bar", "baz": True},
389extra_json={"baz": None},
390),
391)
392data = json.loads(request.content.decode("utf-8"))
393assert data == {"foo": "bar", "baz": None}
394
395def test_request_extra_headers(self) -> None:
396request = self.client._build_request(
397FinalRequestOptions(
398method="post",
399url="/foo",
400**make_request_options(extra_headers={"X-Foo": "Foo"}),
401),
402)
403assert request.headers.get("X-Foo") == "Foo"
404
405# `extra_headers` takes priority over `default_headers` when keys clash
406request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
407FinalRequestOptions(
408method="post",
409url="/foo",
410**make_request_options(
411extra_headers={"X-Bar": "false"},
412),
413),
414)
415assert request.headers.get("X-Bar") == "false"
416
417def test_request_extra_query(self) -> None:
418request = self.client._build_request(
419FinalRequestOptions(
420method="post",
421url="/foo",
422**make_request_options(
423extra_query={"my_query_param": "Foo"},
424),
425),
426)
31573844Stainless Bot2 years ago427params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago428assert params == {"my_query_param": "Foo"}
429
430# if both `query` and `extra_query` are given, they are merged
431request = self.client._build_request(
432FinalRequestOptions(
433method="post",
434url="/foo",
435**make_request_options(
436query={"bar": "1"},
437extra_query={"foo": "2"},
438),
439),
440)
31573844Stainless Bot2 years ago441params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago442assert params == {"bar": "1", "foo": "2"}
443
444# `extra_query` takes priority over `query` when keys clash
445request = self.client._build_request(
446FinalRequestOptions(
447method="post",
448url="/foo",
449**make_request_options(
450query={"foo": "1"},
451extra_query={"foo": "2"},
452),
453),
454)
31573844Stainless Bot2 years ago455params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago456assert params == {"foo": "2"}
457
22713fd0Stainless Bot2 years ago458def test_multipart_repeating_array(self, client: OpenAI) -> None:
459request = client._build_request(
460FinalRequestOptions.construct(
461method="get",
462url="/foo",
463headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
464json_data={"array": ["foo", "bar"]},
465files=[("foo.txt", b"hello world")],
466)
467)
468
469assert request.read().split(b"\r\n") == [
470b"--6b7ba517decee4a450543ea6ae821c82",
471b'Content-Disposition: form-data; name="array[]"',
472b"",
473b"foo",
474b"--6b7ba517decee4a450543ea6ae821c82",
475b'Content-Disposition: form-data; name="array[]"',
476b"",
477b"bar",
478b"--6b7ba517decee4a450543ea6ae821c82",
479b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
480b"Content-Type: application/octet-stream",
481b"",
482b"hello world",
483b"--6b7ba517decee4a450543ea6ae821c82--",
484b"",
485]
486
08b8179aDavid Schnurr2 years ago487@pytest.mark.respx(base_url=base_url)
488def test_basic_union_response(self, respx_mock: MockRouter) -> None:
489class Model1(BaseModel):
490name: str
491
492class Model2(BaseModel):
493foo: str
494
495respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
496
497response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
498assert isinstance(response, Model2)
499assert response.foo == "bar"
500
501@pytest.mark.respx(base_url=base_url)
502def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
503"""Union of objects with the same field name using a different type"""
504
505class Model1(BaseModel):
506foo: int
507
508class Model2(BaseModel):
509foo: str
510
511respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
512
513response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
514assert isinstance(response, Model2)
515assert response.foo == "bar"
516
517respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
518
519response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
520assert isinstance(response, Model1)
521assert response.foo == 1
522
c26014e2Stainless Bot2 years ago523@pytest.mark.respx(base_url=base_url)
524def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
525"""
526Response that sets Content-Type to something other than application/json but returns json data
527"""
528
529class Model(BaseModel):
530foo: int
531
532respx_mock.get("/foo").mock(
533return_value=httpx.Response(
534200,
535content=json.dumps({"foo": 2}),
536headers={"Content-Type": "application/text"},
537)
538)
539
540response = self.client.get("/foo", cast_to=Model)
541assert isinstance(response, Model)
542assert response.foo == 2
543
f6f38a9bStainless Bot2 years ago544def test_base_url_setter(self) -> None:
545client = OpenAI(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True)
546assert client.base_url == "https://example.com/from_init/"
547
548client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
549
550assert client.base_url == "https://example.com/from_setter/"
551
0733934fStainless Bot2 years ago552def test_base_url_env(self) -> None:
553with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
554client = OpenAI(api_key=api_key, _strict_response_validation=True)
555assert client.base_url == "http://localhost:5000/from/env/"
556
08b8179aDavid Schnurr2 years ago557@pytest.mark.parametrize(
558"client",
559[
560OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
561OpenAI(
562base_url="http://localhost:5000/custom/path/",
563api_key=api_key,
564_strict_response_validation=True,
565http_client=httpx.Client(),
566),
567],
568ids=["standard", "custom http client"],
569)
570def test_base_url_trailing_slash(self, client: OpenAI) -> None:
571request = client._build_request(
572FinalRequestOptions(
573method="post",
574url="/foo",
575json_data={"foo": "bar"},
576),
577)
578assert request.url == "http://localhost:5000/custom/path/foo"
579
580@pytest.mark.parametrize(
581"client",
582[
583OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
584OpenAI(
585base_url="http://localhost:5000/custom/path/",
586api_key=api_key,
587_strict_response_validation=True,
588http_client=httpx.Client(),
589),
590],
591ids=["standard", "custom http client"],
592)
593def test_base_url_no_trailing_slash(self, client: OpenAI) -> None:
594request = client._build_request(
595FinalRequestOptions(
596method="post",
597url="/foo",
598json_data={"foo": "bar"},
599),
600)
601assert request.url == "http://localhost:5000/custom/path/foo"
602
603@pytest.mark.parametrize(
604"client",
605[
606OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
607OpenAI(
608base_url="http://localhost:5000/custom/path/",
609api_key=api_key,
610_strict_response_validation=True,
611http_client=httpx.Client(),
612),
613],
614ids=["standard", "custom http client"],
615)
616def test_absolute_request_url(self, client: OpenAI) -> None:
617request = client._build_request(
618FinalRequestOptions(
619method="post",
620url="https://myapi.com/foo",
621json_data={"foo": "bar"},
622),
623)
624assert request.url == "https://myapi.com/foo"
625
626def test_copied_client_does_not_close_http(self) -> None:
627client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
628assert not client.is_closed()
629
630copied = client.copy()
631assert copied is not client
632
a7ebc260Stainless Bot2 years ago633del copied
08b8179aDavid Schnurr2 years ago634
635assert not client.is_closed()
636
637def test_client_context_manager(self) -> None:
638client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
639with client as c2:
640assert c2 is client
641assert not c2.is_closed()
642assert not client.is_closed()
643assert client.is_closed()
644
645@pytest.mark.respx(base_url=base_url)
646def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
647class Model(BaseModel):
648foo: str
649
650respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
651
652with pytest.raises(APIResponseValidationError) as exc:
653self.client.get("/foo", cast_to=Model)
654
655assert isinstance(exc.value.__cause__, ValidationError)
656
07079085Stainless Bot2 years ago657def test_client_max_retries_validation(self) -> None:
658with pytest.raises(TypeError, match=r"max_retries cannot be None"):
659OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None))
660
08b8179aDavid Schnurr2 years ago661@pytest.mark.respx(base_url=base_url)
662def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
663class Model(BaseModel):
664name: str
665
666respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
667
86379b44Stainless Bot2 years ago668stream = self.client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model])
669assert isinstance(stream, Stream)
670stream.response.close()
08b8179aDavid Schnurr2 years ago671
672@pytest.mark.respx(base_url=base_url)
673def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
674class Model(BaseModel):
675name: str
676
677respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
678
679strict_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
680
681with pytest.raises(APIResponseValidationError):
682strict_client.get("/foo", cast_to=Model)
683
684client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
685
686response = client.get("/foo", cast_to=Model)
687assert isinstance(response, str) # type: ignore[unreachable]
688
689@pytest.mark.parametrize(
690"remaining_retries,retry_after,timeout",
691[
692[3, "20", 20],
693[3, "0", 0.5],
694[3, "-10", 0.5],
695[3, "60", 60],
696[3, "61", 0.5],
697[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
698[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
699[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
700[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
701[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
702[3, "99999999999999999999999999999999999", 0.5],
703[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
704[3, "", 0.5],
705[2, "", 0.5 * 2.0],
706[1, "", 0.5 * 4.0],
7f6a921cstainless-app[bot]1 years ago707[-1100, "", 8], # test large number potentially overflowing
08b8179aDavid Schnurr2 years ago708],
709)
710@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
711def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
712client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
713
714headers = httpx.Headers({"retry-after": retry_after})
715options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
716calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
717assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
718
ba4f7a97Stainless Bot2 years ago719@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
720@pytest.mark.respx(base_url=base_url)
721def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
722respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
723
724with pytest.raises(APITimeoutError):
725self.client.post(
726"/chat/completions",
76382e3cStainless Bot2 years ago727body=cast(
728object,
300f58bbstainless-app[bot]1 years ago729maybe_transform(
730dict(
731messages=[
732{
733"role": "user",
734"content": "Say this is a test",
735}
736],
737model="gpt-4o",
738),
739CompletionCreateParamsNonStreaming,
76382e3cStainless Bot2 years ago740),
ba4f7a97Stainless Bot2 years ago741),
742cast_to=httpx.Response,
86379b44Stainless Bot2 years ago743options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
ba4f7a97Stainless Bot2 years ago744)
7aad3405Stainless Bot2 years ago745
746assert _get_open_connections(self.client) == 0
747
ba4f7a97Stainless Bot2 years ago748@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago749@pytest.mark.respx(base_url=base_url)
ba4f7a97Stainless Bot2 years ago750def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
751respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago752
ba4f7a97Stainless Bot2 years ago753with pytest.raises(APIStatusError):
754self.client.post(
755"/chat/completions",
76382e3cStainless Bot2 years ago756body=cast(
757object,
300f58bbstainless-app[bot]1 years ago758maybe_transform(
759dict(
760messages=[
761{
762"role": "user",
763"content": "Say this is a test",
764}
765],
766model="gpt-4o",
767),
768CompletionCreateParamsNonStreaming,
76382e3cStainless Bot2 years ago769),
ba4f7a97Stainless Bot2 years ago770),
771cast_to=httpx.Response,
86379b44Stainless Bot2 years ago772options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
7aad3405Stainless Bot2 years ago773)
774
ba4f7a97Stainless Bot2 years ago775assert _get_open_connections(self.client) == 0
7aad3405Stainless Bot2 years ago776
98d8b2acstainless-app[bot]1 years ago777@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
778@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
779@pytest.mark.respx(base_url=base_url)
6634f525stainless-app[bot]1 years ago780@pytest.mark.parametrize("failure_mode", ["status", "exception"])
781def test_retries_taken(
782self,
783client: OpenAI,
784failures_before_success: int,
785failure_mode: Literal["status", "exception"],
786respx_mock: MockRouter,
787) -> None:
98d8b2acstainless-app[bot]1 years ago788client = client.with_options(max_retries=4)
789
790nb_retries = 0
791
792def retry_handler(_request: httpx.Request) -> httpx.Response:
793nonlocal nb_retries
794if nb_retries < failures_before_success:
795nb_retries += 1
6634f525stainless-app[bot]1 years ago796if failure_mode == "exception":
797raise RuntimeError("oops")
98d8b2acstainless-app[bot]1 years ago798return httpx.Response(500)
799return httpx.Response(200)
800
801respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
802
803response = client.chat.completions.with_raw_response.create(
804messages=[
805{
bf1ca86cRobert Craigie1 years ago806"content": "string",
575ff607stainless-app[bot]1 years ago807"role": "developer",
98d8b2acstainless-app[bot]1 years ago808}
809],
bf1ca86cRobert Craigie1 years ago810model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago811)
812
813assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago814assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
815
816@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
817@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
818@pytest.mark.respx(base_url=base_url)
819def test_omit_retry_count_header(
820self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
821) -> None:
822client = client.with_options(max_retries=4)
823
824nb_retries = 0
825
826def retry_handler(_request: httpx.Request) -> httpx.Response:
827nonlocal nb_retries
828if nb_retries < failures_before_success:
829nb_retries += 1
830return httpx.Response(500)
831return httpx.Response(200)
832
833respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
834
835response = client.chat.completions.with_raw_response.create(
836messages=[
837{
838"content": "string",
575ff607stainless-app[bot]1 years ago839"role": "developer",
5449e208Stainless Bot1 years ago840}
841],
842model="gpt-4o",
843extra_headers={"x-stainless-retry-count": Omit()},
844)
845
846assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
847
848@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
849@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
850@pytest.mark.respx(base_url=base_url)
851def test_overwrite_retry_count_header(
852self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
853) -> None:
854client = client.with_options(max_retries=4)
855
856nb_retries = 0
857
858def retry_handler(_request: httpx.Request) -> httpx.Response:
859nonlocal nb_retries
860if nb_retries < failures_before_success:
861nb_retries += 1
862return httpx.Response(500)
863return httpx.Response(200)
864
865respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
866
867response = client.chat.completions.with_raw_response.create(
868messages=[
869{
870"content": "string",
575ff607stainless-app[bot]1 years ago871"role": "developer",
5449e208Stainless Bot1 years ago872}
873],
874model="gpt-4o",
875extra_headers={"x-stainless-retry-count": "42"},
876)
877
878assert response.http_request.headers.get("x-stainless-retry-count") == "42"
98d8b2acstainless-app[bot]1 years ago879
880@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
881@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
882@pytest.mark.respx(base_url=base_url)
883def test_retries_taken_new_response_class(
884self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
885) -> None:
886client = client.with_options(max_retries=4)
887
888nb_retries = 0
889
890def retry_handler(_request: httpx.Request) -> httpx.Response:
891nonlocal nb_retries
892if nb_retries < failures_before_success:
893nb_retries += 1
894return httpx.Response(500)
895return httpx.Response(200)
896
897respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
898
899with client.chat.completions.with_streaming_response.create(
900messages=[
901{
bf1ca86cRobert Craigie1 years ago902"content": "string",
575ff607stainless-app[bot]1 years ago903"role": "developer",
98d8b2acstainless-app[bot]1 years ago904}
905],
bf1ca86cRobert Craigie1 years ago906model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago907) as response:
908assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago909assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
98d8b2acstainless-app[bot]1 years ago910
cca09707stainless-app[bot]1 years ago911@pytest.mark.respx(base_url=base_url)
912def test_follow_redirects(self, respx_mock: MockRouter) -> None:
913# Test that the default follow_redirects=True allows following redirects
914respx_mock.post("/redirect").mock(
915return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
916)
917respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
918
919response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
920assert response.status_code == 200
921assert response.json() == {"status": "ok"}
922
923@pytest.mark.respx(base_url=base_url)
924def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
925# Test that follow_redirects=False prevents following redirects
926respx_mock.post("/redirect").mock(
927return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
928)
929
930with pytest.raises(APIStatusError) as exc_info:
931self.client.post(
932"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
933)
934
935assert exc_info.value.response.status_code == 302
936assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
937
08b8179aDavid Schnurr2 years ago938
939class TestAsyncOpenAI:
940client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
941
942@pytest.mark.respx(base_url=base_url)
943@pytest.mark.asyncio
944async def test_raw_response(self, respx_mock: MockRouter) -> None:
c5975bd0Stainless Bot2 years ago945respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago946
947response = await self.client.post("/foo", cast_to=httpx.Response)
948assert response.status_code == 200
949assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago950assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago951
952@pytest.mark.respx(base_url=base_url)
953@pytest.mark.asyncio
954async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
955respx_mock.post("/foo").mock(
956return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
957)
958
959response = await self.client.post("/foo", cast_to=httpx.Response)
960assert response.status_code == 200
961assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago962assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago963
964def test_copy(self) -> None:
965copied = self.client.copy()
966assert id(copied) != id(self.client)
967
968copied = self.client.copy(api_key="another My API Key")
969assert copied.api_key == "another My API Key"
970assert self.client.api_key == "My API Key"
971
972def test_copy_default_options(self) -> None:
973# options that have a default are overridden correctly
974copied = self.client.copy(max_retries=7)
975assert copied.max_retries == 7
976assert self.client.max_retries == 2
977
978copied2 = copied.copy(max_retries=6)
979assert copied2.max_retries == 6
980assert copied.max_retries == 7
981
982# timeout
983assert isinstance(self.client.timeout, httpx.Timeout)
984copied = self.client.copy(timeout=None)
985assert copied.timeout is None
986assert isinstance(self.client.timeout, httpx.Timeout)
987
988def test_copy_default_headers(self) -> None:
989client = AsyncOpenAI(
990base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
991)
992assert client.default_headers["X-Foo"] == "bar"
993
994# does not override the already given value when not specified
995copied = client.copy()
996assert copied.default_headers["X-Foo"] == "bar"
997
998# merges already given headers
999copied = client.copy(default_headers={"X-Bar": "stainless"})
1000assert copied.default_headers["X-Foo"] == "bar"
1001assert copied.default_headers["X-Bar"] == "stainless"
1002
1003# uses new values for any already given headers
1004copied = client.copy(default_headers={"X-Foo": "stainless"})
1005assert copied.default_headers["X-Foo"] == "stainless"
1006
1007# set_default_headers
1008
1009# completely overrides already set values
1010copied = client.copy(set_default_headers={})
1011assert copied.default_headers.get("X-Foo") is None
1012
1013copied = client.copy(set_default_headers={"X-Bar": "Robert"})
1014assert copied.default_headers["X-Bar"] == "Robert"
1015
1016with pytest.raises(
1017ValueError,
1018match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
1019):
1020client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
1021
1022def test_copy_default_query(self) -> None:
1023client = AsyncOpenAI(
1024base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
1025)
1026assert _get_params(client)["foo"] == "bar"
1027
1028# does not override the already given value when not specified
1029copied = client.copy()
1030assert _get_params(copied)["foo"] == "bar"
1031
1032# merges already given params
1033copied = client.copy(default_query={"bar": "stainless"})
1034params = _get_params(copied)
1035assert params["foo"] == "bar"
1036assert params["bar"] == "stainless"
1037
1038# uses new values for any already given headers
1039copied = client.copy(default_query={"foo": "stainless"})
1040assert _get_params(copied)["foo"] == "stainless"
1041
1042# set_default_query
1043
1044# completely overrides already set values
1045copied = client.copy(set_default_query={})
1046assert _get_params(copied) == {}
1047
1048copied = client.copy(set_default_query={"bar": "Robert"})
1049assert _get_params(copied)["bar"] == "Robert"
1050
1051with pytest.raises(
1052ValueError,
1053# TODO: update
1054match="`default_query` and `set_default_query` arguments are mutually exclusive",
1055):
1056client.copy(set_default_query={}, default_query={"foo": "Bar"})
1057
1058def test_copy_signature(self) -> None:
1059# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
1060init_signature = inspect.signature(
1061# mypy doesn't like that we access the `__init__` property.
1062self.client.__init__, # type: ignore[misc]
1063)
1064copy_signature = inspect.signature(self.client.copy)
1065exclude_params = {"transport", "proxies", "_strict_response_validation"}
1066
1067for name in init_signature.parameters.keys():
1068if name in exclude_params:
1069continue
1070
1071copy_param = copy_signature.parameters.get(name)
1072assert copy_param is not None, f"copy() signature is missing the {name} param"
1073
d052708aStainless Bot2 years ago1074def test_copy_build_request(self) -> None:
1075options = FinalRequestOptions(method="get", url="/foo")
1076
1077def build_request(options: FinalRequestOptions) -> None:
1078client = self.client.copy()
1079client._build_request(options)
1080
1081# ensure that the machinery is warmed up before tracing starts.
1082build_request(options)
1083gc.collect()
1084
1085tracemalloc.start(1000)
1086
1087snapshot_before = tracemalloc.take_snapshot()
1088
1089ITERATIONS = 10
1090for _ in range(ITERATIONS):
1091build_request(options)
1092
ce04ec28Stainless Bot2 years ago1093gc.collect()
d052708aStainless Bot2 years ago1094snapshot_after = tracemalloc.take_snapshot()
1095
1096tracemalloc.stop()
1097
1098def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
1099if diff.count == 0:
1100# Avoid false positives by considering only leaks (i.e. allocations that persist).
1101return
1102
1103if diff.count % ITERATIONS != 0:
1104# Avoid false positives by considering only leaks that appear per iteration.
1105return
1106
1107for frame in diff.traceback:
1108if any(
1109frame.filename.endswith(fragment)
1110for fragment in [
1111# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
1112#
1113# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago1114"openai/_legacy_response.py",
d052708aStainless Bot2 years ago1115"openai/_response.py",
1116# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
1117"openai/_compat.py",
1118# Standard library leaks we don't care about.
1119"/logging/__init__.py",
1120]
1121):
1122return
1123
1124leaks.append(diff)
1125
1126leaks: list[tracemalloc.StatisticDiff] = []
1127for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
1128add_leak(leaks, diff)
1129if leaks:
1130for leak in leaks:
1131print("MEMORY LEAK:", leak)
1132for frame in leak.traceback:
1133print(frame)
1134raise AssertionError()
1135
08b8179aDavid Schnurr2 years ago1136async def test_request_timeout(self) -> None:
1137request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
1138timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1139assert timeout == DEFAULT_TIMEOUT
1140
1141request = self.client._build_request(
1142FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
1143)
1144timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1145assert timeout == httpx.Timeout(100.0)
1146
1147async def test_client_timeout_option(self) -> None:
1148client = AsyncOpenAI(
1149base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0)
1150)
1151
1152request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1153timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1154assert timeout == httpx.Timeout(0)
1155
1156async def test_http_client_timeout_option(self) -> None:
1157# custom timeout given to the httpx client should be used
1158async with httpx.AsyncClient(timeout=None) as http_client:
1159client = AsyncOpenAI(
1160base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1161)
1162
1163request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1164timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1165assert timeout == httpx.Timeout(None)
1166
1167# no timeout given to the httpx client should not use the httpx default
1168async with httpx.AsyncClient() as http_client:
1169client = AsyncOpenAI(
1170base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1171)
1172
1173request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1174timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1175assert timeout == DEFAULT_TIMEOUT
1176
1177# explicitly passing the default timeout currently results in it being ignored
1178async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
1179client = AsyncOpenAI(
1180base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1181)
1182
1183request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1184timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1185assert timeout == DEFAULT_TIMEOUT # our default
1186
dae0ec80Stainless Bot2 years ago1187def test_invalid_http_client(self) -> None:
1188with pytest.raises(TypeError, match="Invalid `http_client` arg"):
1189with httpx.Client() as http_client:
1190AsyncOpenAI(
1191base_url=base_url,
1192api_key=api_key,
1193_strict_response_validation=True,
1194http_client=cast(Any, http_client),
1195)
1196
08b8179aDavid Schnurr2 years ago1197def test_default_headers_option(self) -> None:
1198client = AsyncOpenAI(
1199base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
1200)
1201request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1202assert request.headers.get("x-foo") == "bar"
1203assert request.headers.get("x-stainless-lang") == "python"
1204
1205client2 = AsyncOpenAI(
1206base_url=base_url,
1207api_key=api_key,
1208_strict_response_validation=True,
1209default_headers={
1210"X-Foo": "stainless",
1211"X-Stainless-Lang": "my-overriding-header",
1212},
1213)
1214request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
1215assert request.headers.get("x-foo") == "stainless"
1216assert request.headers.get("x-stainless-lang") == "my-overriding-header"
1217
1218def test_validate_headers(self) -> None:
1219client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1220request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1221assert request.headers.get("Authorization") == f"Bearer {api_key}"
1222
e967f5a5Stainless Bot2 years ago1223with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago1224with update_env(**{"OPENAI_API_KEY": Omit()}):
1225client2 = AsyncOpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago1226_ = client2
1227
1228def test_default_query_option(self) -> None:
1229client = AsyncOpenAI(
1230base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
1231)
1232request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1233url = httpx.URL(request.url)
1234assert dict(url.params) == {"query_param": "bar"}
1235
1236request = client._build_request(
1237FinalRequestOptions(
1238method="get",
1239url="/foo",
eba67815stainless-app[bot]1 years ago1240params={"foo": "baz", "query_param": "overridden"},
08b8179aDavid Schnurr2 years ago1241)
1242)
1243url = httpx.URL(request.url)
eba67815stainless-app[bot]1 years ago1244assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
08b8179aDavid Schnurr2 years ago1245
1246def test_request_extra_json(self) -> None:
1247request = self.client._build_request(
1248FinalRequestOptions(
1249method="post",
1250url="/foo",
1251json_data={"foo": "bar"},
1252extra_json={"baz": False},
1253),
1254)
1255data = json.loads(request.content.decode("utf-8"))
1256assert data == {"foo": "bar", "baz": False}
1257
1258request = self.client._build_request(
1259FinalRequestOptions(
1260method="post",
1261url="/foo",
1262extra_json={"baz": False},
1263),
1264)
1265data = json.loads(request.content.decode("utf-8"))
1266assert data == {"baz": False}
1267
1268# `extra_json` takes priority over `json_data` when keys clash
1269request = self.client._build_request(
1270FinalRequestOptions(
1271method="post",
1272url="/foo",
1273json_data={"foo": "bar", "baz": True},
1274extra_json={"baz": None},
1275),
1276)
1277data = json.loads(request.content.decode("utf-8"))
1278assert data == {"foo": "bar", "baz": None}
1279
1280def test_request_extra_headers(self) -> None:
1281request = self.client._build_request(
1282FinalRequestOptions(
1283method="post",
1284url="/foo",
1285**make_request_options(extra_headers={"X-Foo": "Foo"}),
1286),
1287)
1288assert request.headers.get("X-Foo") == "Foo"
1289
1290# `extra_headers` takes priority over `default_headers` when keys clash
1291request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
1292FinalRequestOptions(
1293method="post",
1294url="/foo",
1295**make_request_options(
1296extra_headers={"X-Bar": "false"},
1297),
1298),
1299)
1300assert request.headers.get("X-Bar") == "false"
1301
1302def test_request_extra_query(self) -> None:
1303request = self.client._build_request(
1304FinalRequestOptions(
1305method="post",
1306url="/foo",
1307**make_request_options(
1308extra_query={"my_query_param": "Foo"},
1309),
1310),
1311)
31573844Stainless Bot2 years ago1312params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1313assert params == {"my_query_param": "Foo"}
1314
1315# if both `query` and `extra_query` are given, they are merged
1316request = self.client._build_request(
1317FinalRequestOptions(
1318method="post",
1319url="/foo",
1320**make_request_options(
1321query={"bar": "1"},
1322extra_query={"foo": "2"},
1323),
1324),
1325)
31573844Stainless Bot2 years ago1326params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1327assert params == {"bar": "1", "foo": "2"}
1328
1329# `extra_query` takes priority over `query` when keys clash
1330request = self.client._build_request(
1331FinalRequestOptions(
1332method="post",
1333url="/foo",
1334**make_request_options(
1335query={"foo": "1"},
1336extra_query={"foo": "2"},
1337),
1338),
1339)
31573844Stainless Bot2 years ago1340params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1341assert params == {"foo": "2"}
1342
22713fd0Stainless Bot2 years ago1343def test_multipart_repeating_array(self, async_client: AsyncOpenAI) -> None:
1344request = async_client._build_request(
1345FinalRequestOptions.construct(
1346method="get",
1347url="/foo",
1348headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
1349json_data={"array": ["foo", "bar"]},
1350files=[("foo.txt", b"hello world")],
1351)
1352)
1353
1354assert request.read().split(b"\r\n") == [
1355b"--6b7ba517decee4a450543ea6ae821c82",
1356b'Content-Disposition: form-data; name="array[]"',
1357b"",
1358b"foo",
1359b"--6b7ba517decee4a450543ea6ae821c82",
1360b'Content-Disposition: form-data; name="array[]"',
1361b"",
1362b"bar",
1363b"--6b7ba517decee4a450543ea6ae821c82",
1364b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
1365b"Content-Type: application/octet-stream",
1366b"",
1367b"hello world",
1368b"--6b7ba517decee4a450543ea6ae821c82--",
1369b"",
1370]
1371
08b8179aDavid Schnurr2 years ago1372@pytest.mark.respx(base_url=base_url)
1373async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
1374class Model1(BaseModel):
1375name: str
1376
1377class Model2(BaseModel):
1378foo: str
1379
1380respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1381
1382response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1383assert isinstance(response, Model2)
1384assert response.foo == "bar"
1385
1386@pytest.mark.respx(base_url=base_url)
1387async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
1388"""Union of objects with the same field name using a different type"""
1389
1390class Model1(BaseModel):
1391foo: int
1392
1393class Model2(BaseModel):
1394foo: str
1395
1396respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1397
1398response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1399assert isinstance(response, Model2)
1400assert response.foo == "bar"
1401
1402respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
1403
1404response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1405assert isinstance(response, Model1)
1406assert response.foo == 1
1407
c26014e2Stainless Bot2 years ago1408@pytest.mark.respx(base_url=base_url)
1409async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
1410"""
1411Response that sets Content-Type to something other than application/json but returns json data
1412"""
1413
1414class Model(BaseModel):
1415foo: int
1416
1417respx_mock.get("/foo").mock(
1418return_value=httpx.Response(
1419200,
1420content=json.dumps({"foo": 2}),
1421headers={"Content-Type": "application/text"},
1422)
1423)
1424
1425response = await self.client.get("/foo", cast_to=Model)
1426assert isinstance(response, Model)
1427assert response.foo == 2
1428
f6f38a9bStainless Bot2 years ago1429def test_base_url_setter(self) -> None:
1430client = AsyncOpenAI(
1431base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True
1432)
1433assert client.base_url == "https://example.com/from_init/"
1434
1435client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
1436
1437assert client.base_url == "https://example.com/from_setter/"
1438
0733934fStainless Bot2 years ago1439def test_base_url_env(self) -> None:
1440with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
1441client = AsyncOpenAI(api_key=api_key, _strict_response_validation=True)
1442assert client.base_url == "http://localhost:5000/from/env/"
1443
08b8179aDavid Schnurr2 years ago1444@pytest.mark.parametrize(
1445"client",
1446[
1447AsyncOpenAI(
1448base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1449),
1450AsyncOpenAI(
1451base_url="http://localhost:5000/custom/path/",
1452api_key=api_key,
1453_strict_response_validation=True,
1454http_client=httpx.AsyncClient(),
1455),
1456],
1457ids=["standard", "custom http client"],
1458)
1459def test_base_url_trailing_slash(self, client: AsyncOpenAI) -> None:
1460request = client._build_request(
1461FinalRequestOptions(
1462method="post",
1463url="/foo",
1464json_data={"foo": "bar"},
1465),
1466)
1467assert request.url == "http://localhost:5000/custom/path/foo"
1468
1469@pytest.mark.parametrize(
1470"client",
1471[
1472AsyncOpenAI(
1473base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1474),
1475AsyncOpenAI(
1476base_url="http://localhost:5000/custom/path/",
1477api_key=api_key,
1478_strict_response_validation=True,
1479http_client=httpx.AsyncClient(),
1480),
1481],
1482ids=["standard", "custom http client"],
1483)
1484def test_base_url_no_trailing_slash(self, client: AsyncOpenAI) -> None:
1485request = client._build_request(
1486FinalRequestOptions(
1487method="post",
1488url="/foo",
1489json_data={"foo": "bar"},
1490),
1491)
1492assert request.url == "http://localhost:5000/custom/path/foo"
1493
1494@pytest.mark.parametrize(
1495"client",
1496[
1497AsyncOpenAI(
1498base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1499),
1500AsyncOpenAI(
1501base_url="http://localhost:5000/custom/path/",
1502api_key=api_key,
1503_strict_response_validation=True,
1504http_client=httpx.AsyncClient(),
1505),
1506],
1507ids=["standard", "custom http client"],
1508)
1509def test_absolute_request_url(self, client: AsyncOpenAI) -> None:
1510request = client._build_request(
1511FinalRequestOptions(
1512method="post",
1513url="https://myapi.com/foo",
1514json_data={"foo": "bar"},
1515),
1516)
1517assert request.url == "https://myapi.com/foo"
1518
1519async def test_copied_client_does_not_close_http(self) -> None:
1520client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1521assert not client.is_closed()
1522
1523copied = client.copy()
1524assert copied is not client
1525
a7ebc260Stainless Bot2 years ago1526del copied
08b8179aDavid Schnurr2 years ago1527
1528await asyncio.sleep(0.2)
1529assert not client.is_closed()
1530
1531async def test_client_context_manager(self) -> None:
1532client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1533async with client as c2:
1534assert c2 is client
1535assert not c2.is_closed()
1536assert not client.is_closed()
1537assert client.is_closed()
1538
1539@pytest.mark.respx(base_url=base_url)
1540@pytest.mark.asyncio
1541async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
1542class Model(BaseModel):
1543foo: str
1544
1545respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
1546
1547with pytest.raises(APIResponseValidationError) as exc:
1548await self.client.get("/foo", cast_to=Model)
1549
1550assert isinstance(exc.value.__cause__, ValidationError)
1551
07079085Stainless Bot2 years ago1552async def test_client_max_retries_validation(self) -> None:
1553with pytest.raises(TypeError, match=r"max_retries cannot be None"):
1554AsyncOpenAI(
1555base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)
1556)
1557
08b8179aDavid Schnurr2 years ago1558@pytest.mark.respx(base_url=base_url)
1559@pytest.mark.asyncio
1560async def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
1561class Model(BaseModel):
1562name: str
1563
1564respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1565
86379b44Stainless Bot2 years ago1566stream = await self.client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
1567assert isinstance(stream, AsyncStream)
1568await stream.response.aclose()
08b8179aDavid Schnurr2 years ago1569
1570@pytest.mark.respx(base_url=base_url)
1571@pytest.mark.asyncio
1572async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
1573class Model(BaseModel):
1574name: str
1575
1576respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
1577
1578strict_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1579
1580with pytest.raises(APIResponseValidationError):
1581await strict_client.get("/foo", cast_to=Model)
1582
1583client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
1584
1585response = await client.get("/foo", cast_to=Model)
1586assert isinstance(response, str) # type: ignore[unreachable]
1587
1588@pytest.mark.parametrize(
1589"remaining_retries,retry_after,timeout",
1590[
1591[3, "20", 20],
1592[3, "0", 0.5],
1593[3, "-10", 0.5],
1594[3, "60", 60],
1595[3, "61", 0.5],
1596[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
1597[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
1598[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
1599[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
1600[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
1601[3, "99999999999999999999999999999999999", 0.5],
1602[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
1603[3, "", 0.5],
1604[2, "", 0.5 * 2.0],
1605[1, "", 0.5 * 4.0],
7f6a921cstainless-app[bot]1 years ago1606[-1100, "", 8], # test large number potentially overflowing
08b8179aDavid Schnurr2 years ago1607],
1608)
1609@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
1610@pytest.mark.asyncio
1611async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
1612client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1613
1614headers = httpx.Headers({"retry-after": retry_after})
1615options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
1616calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
1617assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
7aad3405Stainless Bot2 years ago1618
ba4f7a97Stainless Bot2 years ago1619@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1620@pytest.mark.respx(base_url=base_url)
1621async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
1622respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
1623
1624with pytest.raises(APITimeoutError):
1625await self.client.post(
1626"/chat/completions",
76382e3cStainless Bot2 years ago1627body=cast(
1628object,
300f58bbstainless-app[bot]1 years ago1629maybe_transform(
1630dict(
1631messages=[
1632{
1633"role": "user",
1634"content": "Say this is a test",
1635}
1636],
1637model="gpt-4o",
1638),
1639CompletionCreateParamsNonStreaming,
76382e3cStainless Bot2 years ago1640),
ba4f7a97Stainless Bot2 years ago1641),
1642cast_to=httpx.Response,
86379b44Stainless Bot2 years ago1643options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
ba4f7a97Stainless Bot2 years ago1644)
7aad3405Stainless Bot2 years ago1645
1646assert _get_open_connections(self.client) == 0
1647
ba4f7a97Stainless Bot2 years ago1648@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago1649@pytest.mark.respx(base_url=base_url)
ba4f7a97Stainless Bot2 years ago1650async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
1651respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago1652
ba4f7a97Stainless Bot2 years ago1653with pytest.raises(APIStatusError):
1654await self.client.post(
1655"/chat/completions",
76382e3cStainless Bot2 years ago1656body=cast(
1657object,
300f58bbstainless-app[bot]1 years ago1658maybe_transform(
1659dict(
1660messages=[
1661{
1662"role": "user",
1663"content": "Say this is a test",
1664}
1665],
1666model="gpt-4o",
1667),
1668CompletionCreateParamsNonStreaming,
76382e3cStainless Bot2 years ago1669),
ba4f7a97Stainless Bot2 years ago1670),
1671cast_to=httpx.Response,
86379b44Stainless Bot2 years ago1672options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
7aad3405Stainless Bot2 years ago1673)
1674
ba4f7a97Stainless Bot2 years ago1675assert _get_open_connections(self.client) == 0
98d8b2acstainless-app[bot]1 years ago1676
1677@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1678@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1679@pytest.mark.respx(base_url=base_url)
1680@pytest.mark.asyncio
6634f525stainless-app[bot]1 years ago1681@pytest.mark.parametrize("failure_mode", ["status", "exception"])
98d8b2acstainless-app[bot]1 years ago1682async def test_retries_taken(
6634f525stainless-app[bot]1 years ago1683self,
1684async_client: AsyncOpenAI,
1685failures_before_success: int,
1686failure_mode: Literal["status", "exception"],
1687respx_mock: MockRouter,
98d8b2acstainless-app[bot]1 years ago1688) -> None:
1689client = async_client.with_options(max_retries=4)
1690
1691nb_retries = 0
1692
1693def retry_handler(_request: httpx.Request) -> httpx.Response:
1694nonlocal nb_retries
1695if nb_retries < failures_before_success:
1696nb_retries += 1
6634f525stainless-app[bot]1 years ago1697if failure_mode == "exception":
1698raise RuntimeError("oops")
98d8b2acstainless-app[bot]1 years ago1699return httpx.Response(500)
1700return httpx.Response(200)
1701
1702respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1703
1704response = await client.chat.completions.with_raw_response.create(
1705messages=[
1706{
bf1ca86cRobert Craigie1 years ago1707"content": "string",
575ff607stainless-app[bot]1 years ago1708"role": "developer",
98d8b2acstainless-app[bot]1 years ago1709}
1710],
bf1ca86cRobert Craigie1 years ago1711model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago1712)
1713
1714assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago1715assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
1716
1717@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1718@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1719@pytest.mark.respx(base_url=base_url)
1720@pytest.mark.asyncio
1721async def test_omit_retry_count_header(
1722self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1723) -> None:
1724client = async_client.with_options(max_retries=4)
1725
1726nb_retries = 0
1727
1728def retry_handler(_request: httpx.Request) -> httpx.Response:
1729nonlocal nb_retries
1730if nb_retries < failures_before_success:
1731nb_retries += 1
1732return httpx.Response(500)
1733return httpx.Response(200)
1734
1735respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1736
1737response = await client.chat.completions.with_raw_response.create(
1738messages=[
1739{
1740"content": "string",
575ff607stainless-app[bot]1 years ago1741"role": "developer",
5449e208Stainless Bot1 years ago1742}
1743],
1744model="gpt-4o",
1745extra_headers={"x-stainless-retry-count": Omit()},
1746)
1747
1748assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
1749
1750@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1751@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1752@pytest.mark.respx(base_url=base_url)
1753@pytest.mark.asyncio
1754async def test_overwrite_retry_count_header(
1755self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1756) -> None:
1757client = async_client.with_options(max_retries=4)
1758
1759nb_retries = 0
1760
1761def retry_handler(_request: httpx.Request) -> httpx.Response:
1762nonlocal nb_retries
1763if nb_retries < failures_before_success:
1764nb_retries += 1
1765return httpx.Response(500)
1766return httpx.Response(200)
1767
1768respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1769
1770response = await client.chat.completions.with_raw_response.create(
1771messages=[
1772{
1773"content": "string",
575ff607stainless-app[bot]1 years ago1774"role": "developer",
5449e208Stainless Bot1 years ago1775}
1776],
1777model="gpt-4o",
1778extra_headers={"x-stainless-retry-count": "42"},
1779)
1780
1781assert response.http_request.headers.get("x-stainless-retry-count") == "42"
98d8b2acstainless-app[bot]1 years ago1782
1783@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1784@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1785@pytest.mark.respx(base_url=base_url)
1786@pytest.mark.asyncio
1787async def test_retries_taken_new_response_class(
1788self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1789) -> None:
1790client = async_client.with_options(max_retries=4)
1791
1792nb_retries = 0
1793
1794def retry_handler(_request: httpx.Request) -> httpx.Response:
1795nonlocal nb_retries
1796if nb_retries < failures_before_success:
1797nb_retries += 1
1798return httpx.Response(500)
1799return httpx.Response(200)
1800
1801respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1802
1803async with client.chat.completions.with_streaming_response.create(
1804messages=[
1805{
bf1ca86cRobert Craigie1 years ago1806"content": "string",
575ff607stainless-app[bot]1 years ago1807"role": "developer",
98d8b2acstainless-app[bot]1 years ago1808}
1809],
bf1ca86cRobert Craigie1 years ago1810model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago1811) as response:
1812assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago1813assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
d8901d28Seth Gilchrist1 years ago1814
1815def test_get_platform(self) -> None:
53ab0467Stainless Bot1 years ago1816# A previous implementation of asyncify could leave threads unterminated when
1817# used with nest_asyncio.
1818#
d8901d28Seth Gilchrist1 years ago1819# Since nest_asyncio.apply() is global and cannot be un-applied, this
1820# test is run in a separate process to avoid affecting other tests.
53ab0467Stainless Bot1 years ago1821test_code = dedent("""
d8901d28Seth Gilchrist1 years ago1822import asyncio
1823import nest_asyncio
1824import threading
1825
1826from openai._utils import asyncify
fb69e674stainless-app[bot]1 years ago1827from openai._base_client import get_platform
d8901d28Seth Gilchrist1 years ago1828
1829async def test_main() -> None:
1830result = await asyncify(get_platform)()
1831print(result)
1832for thread in threading.enumerate():
1833print(thread.name)
1834
1835nest_asyncio.apply()
1836asyncio.run(test_main())
1837""")
1838with subprocess.Popen(
1839[sys.executable, "-c", test_code],
1840text=True,
1841) as process:
14543c59stainless-app[bot]1 years ago1842timeout = 10 # seconds
1843
1844start_time = time.monotonic()
1845while True:
1846return_code = process.poll()
1847if return_code is not None:
1848if return_code != 0:
1849raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
1850
1851# success
1852break
1853
1854if time.monotonic() - start_time > timeout:
1855process.kill()
1856raise AssertionError("calling get_platform using asyncify resulted in a hung process")
1857
1858time.sleep(0.1)
cca09707stainless-app[bot]1 years ago1859
1860@pytest.mark.respx(base_url=base_url)
1861async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
1862# Test that the default follow_redirects=True allows following redirects
1863respx_mock.post("/redirect").mock(
1864return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1865)
1866respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
1867
1868response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
1869assert response.status_code == 200
1870assert response.json() == {"status": "ok"}
1871
1872@pytest.mark.respx(base_url=base_url)
1873async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
1874# Test that follow_redirects=False prevents following redirects
1875respx_mock.post("/redirect").mock(
1876return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1877)
1878
1879with pytest.raises(APIStatusError) as exc_info:
1880await self.client.post(
1881"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
1882)
1883
1884assert exc_info.value.response.status_code == 302
1885assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"