openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.96.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/test_client.py

1889lines · 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
18e0b36astainless-app[bot]1 years ago195@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
d052708aStainless Bot2 years ago196def test_copy_build_request(self) -> None:
197options = FinalRequestOptions(method="get", url="/foo")
198
199def build_request(options: FinalRequestOptions) -> None:
200client = self.client.copy()
201client._build_request(options)
202
203# ensure that the machinery is warmed up before tracing starts.
204build_request(options)
205gc.collect()
206
207tracemalloc.start(1000)
208
209snapshot_before = tracemalloc.take_snapshot()
210
211ITERATIONS = 10
212for _ in range(ITERATIONS):
213build_request(options)
214
ce04ec28Stainless Bot2 years ago215gc.collect()
d052708aStainless Bot2 years ago216snapshot_after = tracemalloc.take_snapshot()
217
218tracemalloc.stop()
219
220def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
221if diff.count == 0:
222# Avoid false positives by considering only leaks (i.e. allocations that persist).
223return
224
225if diff.count % ITERATIONS != 0:
226# Avoid false positives by considering only leaks that appear per iteration.
227return
228
229for frame in diff.traceback:
230if any(
231frame.filename.endswith(fragment)
232for fragment in [
233# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
234#
235# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago236"openai/_legacy_response.py",
d052708aStainless Bot2 years ago237"openai/_response.py",
238# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
239"openai/_compat.py",
240# Standard library leaks we don't care about.
241"/logging/__init__.py",
242]
243):
244return
245
246leaks.append(diff)
247
248leaks: list[tracemalloc.StatisticDiff] = []
249for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
250add_leak(leaks, diff)
251if leaks:
252for leak in leaks:
253print("MEMORY LEAK:", leak)
254for frame in leak.traceback:
255print(frame)
256raise AssertionError()
257
08b8179aDavid Schnurr2 years ago258def test_request_timeout(self) -> None:
259request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
260timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
261assert timeout == DEFAULT_TIMEOUT
262
263request = self.client._build_request(
264FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
265)
266timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
267assert timeout == httpx.Timeout(100.0)
268
269def test_client_timeout_option(self) -> None:
270client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0))
271
272request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
273timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
274assert timeout == httpx.Timeout(0)
275
276def test_http_client_timeout_option(self) -> None:
277# custom timeout given to the httpx client should be used
278with httpx.Client(timeout=None) as http_client:
279client = OpenAI(
280base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
281)
282
283request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
284timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
285assert timeout == httpx.Timeout(None)
286
287# no timeout given to the httpx client should not use the httpx default
288with httpx.Client() as http_client:
289client = OpenAI(
290base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
291)
292
293request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
294timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
295assert timeout == DEFAULT_TIMEOUT
296
297# explicitly passing the default timeout currently results in it being ignored
298with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
299client = OpenAI(
300base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
301)
302
303request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
304timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
305assert timeout == DEFAULT_TIMEOUT # our default
306
dae0ec80Stainless Bot2 years ago307async def test_invalid_http_client(self) -> None:
308with pytest.raises(TypeError, match="Invalid `http_client` arg"):
309async with httpx.AsyncClient() as http_client:
310OpenAI(
311base_url=base_url,
312api_key=api_key,
313_strict_response_validation=True,
314http_client=cast(Any, http_client),
315)
316
08b8179aDavid Schnurr2 years ago317def test_default_headers_option(self) -> None:
318client = OpenAI(
319base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
320)
321request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
322assert request.headers.get("x-foo") == "bar"
323assert request.headers.get("x-stainless-lang") == "python"
324
325client2 = OpenAI(
326base_url=base_url,
327api_key=api_key,
328_strict_response_validation=True,
329default_headers={
330"X-Foo": "stainless",
331"X-Stainless-Lang": "my-overriding-header",
332},
333)
334request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
335assert request.headers.get("x-foo") == "stainless"
336assert request.headers.get("x-stainless-lang") == "my-overriding-header"
337
338def test_validate_headers(self) -> None:
339client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
340request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
341assert request.headers.get("Authorization") == f"Bearer {api_key}"
342
e967f5a5Stainless Bot2 years ago343with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago344with update_env(**{"OPENAI_API_KEY": Omit()}):
345client2 = OpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago346_ = client2
347
348def test_default_query_option(self) -> None:
349client = OpenAI(
350base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
351)
352request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
353url = httpx.URL(request.url)
354assert dict(url.params) == {"query_param": "bar"}
355
356request = client._build_request(
357FinalRequestOptions(
358method="get",
359url="/foo",
eba67815stainless-app[bot]1 years ago360params={"foo": "baz", "query_param": "overridden"},
08b8179aDavid Schnurr2 years ago361)
362)
363url = httpx.URL(request.url)
eba67815stainless-app[bot]1 years ago364assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
08b8179aDavid Schnurr2 years ago365
366def test_request_extra_json(self) -> None:
367request = self.client._build_request(
368FinalRequestOptions(
369method="post",
370url="/foo",
371json_data={"foo": "bar"},
372extra_json={"baz": False},
373),
374)
375data = json.loads(request.content.decode("utf-8"))
376assert data == {"foo": "bar", "baz": False}
377
378request = self.client._build_request(
379FinalRequestOptions(
380method="post",
381url="/foo",
382extra_json={"baz": False},
383),
384)
385data = json.loads(request.content.decode("utf-8"))
386assert data == {"baz": False}
387
388# `extra_json` takes priority over `json_data` when keys clash
389request = self.client._build_request(
390FinalRequestOptions(
391method="post",
392url="/foo",
393json_data={"foo": "bar", "baz": True},
394extra_json={"baz": None},
395),
396)
397data = json.loads(request.content.decode("utf-8"))
398assert data == {"foo": "bar", "baz": None}
399
400def test_request_extra_headers(self) -> None:
401request = self.client._build_request(
402FinalRequestOptions(
403method="post",
404url="/foo",
405**make_request_options(extra_headers={"X-Foo": "Foo"}),
406),
407)
408assert request.headers.get("X-Foo") == "Foo"
409
410# `extra_headers` takes priority over `default_headers` when keys clash
411request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
412FinalRequestOptions(
413method="post",
414url="/foo",
415**make_request_options(
416extra_headers={"X-Bar": "false"},
417),
418),
419)
420assert request.headers.get("X-Bar") == "false"
421
422def test_request_extra_query(self) -> None:
423request = self.client._build_request(
424FinalRequestOptions(
425method="post",
426url="/foo",
427**make_request_options(
428extra_query={"my_query_param": "Foo"},
429),
430),
431)
31573844Stainless Bot2 years ago432params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago433assert params == {"my_query_param": "Foo"}
434
435# if both `query` and `extra_query` are given, they are merged
436request = self.client._build_request(
437FinalRequestOptions(
438method="post",
439url="/foo",
440**make_request_options(
441query={"bar": "1"},
442extra_query={"foo": "2"},
443),
444),
445)
31573844Stainless Bot2 years ago446params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago447assert params == {"bar": "1", "foo": "2"}
448
449# `extra_query` takes priority over `query` when keys clash
450request = self.client._build_request(
451FinalRequestOptions(
452method="post",
453url="/foo",
454**make_request_options(
455query={"foo": "1"},
456extra_query={"foo": "2"},
457),
458),
459)
31573844Stainless Bot2 years ago460params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago461assert params == {"foo": "2"}
462
22713fd0Stainless Bot2 years ago463def test_multipart_repeating_array(self, client: OpenAI) -> None:
464request = client._build_request(
465FinalRequestOptions.construct(
fcbb5983stainless-app[bot]11 months ago466method="post",
22713fd0Stainless Bot2 years ago467url="/foo",
468headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
469json_data={"array": ["foo", "bar"]},
470files=[("foo.txt", b"hello world")],
471)
472)
473
474assert request.read().split(b"\r\n") == [
475b"--6b7ba517decee4a450543ea6ae821c82",
476b'Content-Disposition: form-data; name="array[]"',
477b"",
478b"foo",
479b"--6b7ba517decee4a450543ea6ae821c82",
480b'Content-Disposition: form-data; name="array[]"',
481b"",
482b"bar",
483b"--6b7ba517decee4a450543ea6ae821c82",
484b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
485b"Content-Type: application/octet-stream",
486b"",
487b"hello world",
488b"--6b7ba517decee4a450543ea6ae821c82--",
489b"",
490]
491
08b8179aDavid Schnurr2 years ago492@pytest.mark.respx(base_url=base_url)
493def test_basic_union_response(self, respx_mock: MockRouter) -> None:
494class Model1(BaseModel):
495name: str
496
497class Model2(BaseModel):
498foo: str
499
500respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
501
502response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
503assert isinstance(response, Model2)
504assert response.foo == "bar"
505
506@pytest.mark.respx(base_url=base_url)
507def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
508"""Union of objects with the same field name using a different type"""
509
510class Model1(BaseModel):
511foo: int
512
513class Model2(BaseModel):
514foo: str
515
516respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
517
518response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
519assert isinstance(response, Model2)
520assert response.foo == "bar"
521
522respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
523
524response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
525assert isinstance(response, Model1)
526assert response.foo == 1
527
c26014e2Stainless Bot2 years ago528@pytest.mark.respx(base_url=base_url)
529def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
530"""
531Response that sets Content-Type to something other than application/json but returns json data
532"""
533
534class Model(BaseModel):
535foo: int
536
537respx_mock.get("/foo").mock(
538return_value=httpx.Response(
539200,
540content=json.dumps({"foo": 2}),
541headers={"Content-Type": "application/text"},
542)
543)
544
545response = self.client.get("/foo", cast_to=Model)
546assert isinstance(response, Model)
547assert response.foo == 2
548
f6f38a9bStainless Bot2 years ago549def test_base_url_setter(self) -> None:
550client = OpenAI(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True)
551assert client.base_url == "https://example.com/from_init/"
552
553client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
554
555assert client.base_url == "https://example.com/from_setter/"
556
0733934fStainless Bot2 years ago557def test_base_url_env(self) -> None:
558with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
559client = OpenAI(api_key=api_key, _strict_response_validation=True)
560assert client.base_url == "http://localhost:5000/from/env/"
561
08b8179aDavid Schnurr2 years ago562@pytest.mark.parametrize(
563"client",
564[
565OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
566OpenAI(
567base_url="http://localhost:5000/custom/path/",
568api_key=api_key,
569_strict_response_validation=True,
570http_client=httpx.Client(),
571),
572],
573ids=["standard", "custom http client"],
574)
575def test_base_url_trailing_slash(self, client: OpenAI) -> None:
576request = client._build_request(
577FinalRequestOptions(
578method="post",
579url="/foo",
580json_data={"foo": "bar"},
581),
582)
583assert request.url == "http://localhost:5000/custom/path/foo"
584
585@pytest.mark.parametrize(
586"client",
587[
588OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
589OpenAI(
590base_url="http://localhost:5000/custom/path/",
591api_key=api_key,
592_strict_response_validation=True,
593http_client=httpx.Client(),
594),
595],
596ids=["standard", "custom http client"],
597)
598def test_base_url_no_trailing_slash(self, client: OpenAI) -> None:
599request = client._build_request(
600FinalRequestOptions(
601method="post",
602url="/foo",
603json_data={"foo": "bar"},
604),
605)
606assert request.url == "http://localhost:5000/custom/path/foo"
607
608@pytest.mark.parametrize(
609"client",
610[
611OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
612OpenAI(
613base_url="http://localhost:5000/custom/path/",
614api_key=api_key,
615_strict_response_validation=True,
616http_client=httpx.Client(),
617),
618],
619ids=["standard", "custom http client"],
620)
621def test_absolute_request_url(self, client: OpenAI) -> None:
622request = client._build_request(
623FinalRequestOptions(
624method="post",
625url="https://myapi.com/foo",
626json_data={"foo": "bar"},
627),
628)
629assert request.url == "https://myapi.com/foo"
630
631def test_copied_client_does_not_close_http(self) -> None:
632client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
633assert not client.is_closed()
634
635copied = client.copy()
636assert copied is not client
637
a7ebc260Stainless Bot2 years ago638del copied
08b8179aDavid Schnurr2 years ago639
640assert not client.is_closed()
641
642def test_client_context_manager(self) -> None:
643client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
644with client as c2:
645assert c2 is client
646assert not c2.is_closed()
647assert not client.is_closed()
648assert client.is_closed()
649
650@pytest.mark.respx(base_url=base_url)
651def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
652class Model(BaseModel):
653foo: str
654
655respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
656
657with pytest.raises(APIResponseValidationError) as exc:
658self.client.get("/foo", cast_to=Model)
659
660assert isinstance(exc.value.__cause__, ValidationError)
661
07079085Stainless Bot2 years ago662def test_client_max_retries_validation(self) -> None:
663with pytest.raises(TypeError, match=r"max_retries cannot be None"):
664OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None))
665
08b8179aDavid Schnurr2 years ago666@pytest.mark.respx(base_url=base_url)
667def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
668class Model(BaseModel):
669name: str
670
671respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
672
86379b44Stainless Bot2 years ago673stream = self.client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model])
674assert isinstance(stream, Stream)
675stream.response.close()
08b8179aDavid Schnurr2 years ago676
677@pytest.mark.respx(base_url=base_url)
678def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
679class Model(BaseModel):
680name: str
681
682respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
683
684strict_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
685
686with pytest.raises(APIResponseValidationError):
687strict_client.get("/foo", cast_to=Model)
688
689client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
690
691response = client.get("/foo", cast_to=Model)
692assert isinstance(response, str) # type: ignore[unreachable]
693
694@pytest.mark.parametrize(
695"remaining_retries,retry_after,timeout",
696[
697[3, "20", 20],
698[3, "0", 0.5],
699[3, "-10", 0.5],
700[3, "60", 60],
701[3, "61", 0.5],
702[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
703[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
704[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
705[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
706[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
707[3, "99999999999999999999999999999999999", 0.5],
708[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
709[3, "", 0.5],
710[2, "", 0.5 * 2.0],
711[1, "", 0.5 * 4.0],
7f6a921cstainless-app[bot]1 years ago712[-1100, "", 8], # test large number potentially overflowing
08b8179aDavid Schnurr2 years ago713],
714)
715@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
716def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
717client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
718
719headers = httpx.Headers({"retry-after": retry_after})
720options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
721calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
722assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
723
ba4f7a97Stainless Bot2 years ago724@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
725@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago726def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: OpenAI) -> None:
ba4f7a97Stainless Bot2 years ago727respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
728
729with pytest.raises(APITimeoutError):
0bef1d02stainless-app[bot]1 years ago730client.chat.completions.with_streaming_response.create(
731messages=[
732{
733"content": "string",
734"role": "developer",
735}
736],
737model="gpt-4o",
738).__enter__()
7aad3405Stainless Bot2 years ago739
740assert _get_open_connections(self.client) == 0
741
ba4f7a97Stainless Bot2 years ago742@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago743@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago744def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: OpenAI) -> None:
ba4f7a97Stainless Bot2 years ago745respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago746
ba4f7a97Stainless Bot2 years ago747with pytest.raises(APIStatusError):
0bef1d02stainless-app[bot]1 years ago748client.chat.completions.with_streaming_response.create(
749messages=[
750{
751"content": "string",
752"role": "developer",
753}
754],
755model="gpt-4o",
756).__enter__()
ba4f7a97Stainless Bot2 years ago757assert _get_open_connections(self.client) == 0
7aad3405Stainless Bot2 years ago758
98d8b2acstainless-app[bot]1 years ago759@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
760@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
761@pytest.mark.respx(base_url=base_url)
6634f525stainless-app[bot]1 years ago762@pytest.mark.parametrize("failure_mode", ["status", "exception"])
763def test_retries_taken(
764self,
765client: OpenAI,
766failures_before_success: int,
767failure_mode: Literal["status", "exception"],
768respx_mock: MockRouter,
769) -> None:
98d8b2acstainless-app[bot]1 years ago770client = client.with_options(max_retries=4)
771
772nb_retries = 0
773
774def retry_handler(_request: httpx.Request) -> httpx.Response:
775nonlocal nb_retries
776if nb_retries < failures_before_success:
777nb_retries += 1
6634f525stainless-app[bot]1 years ago778if failure_mode == "exception":
779raise RuntimeError("oops")
98d8b2acstainless-app[bot]1 years ago780return httpx.Response(500)
781return httpx.Response(200)
782
783respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
784
785response = client.chat.completions.with_raw_response.create(
786messages=[
787{
bf1ca86cRobert Craigie1 years ago788"content": "string",
575ff607stainless-app[bot]1 years ago789"role": "developer",
98d8b2acstainless-app[bot]1 years ago790}
791],
bf1ca86cRobert Craigie1 years ago792model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago793)
794
795assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago796assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
797
798@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
799@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
800@pytest.mark.respx(base_url=base_url)
801def test_omit_retry_count_header(
802self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
803) -> None:
804client = client.with_options(max_retries=4)
805
806nb_retries = 0
807
808def retry_handler(_request: httpx.Request) -> httpx.Response:
809nonlocal nb_retries
810if nb_retries < failures_before_success:
811nb_retries += 1
812return httpx.Response(500)
813return httpx.Response(200)
814
815respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
816
817response = client.chat.completions.with_raw_response.create(
818messages=[
819{
820"content": "string",
575ff607stainless-app[bot]1 years ago821"role": "developer",
5449e208Stainless Bot1 years ago822}
823],
824model="gpt-4o",
825extra_headers={"x-stainless-retry-count": Omit()},
826)
827
828assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
829
830@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
831@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
832@pytest.mark.respx(base_url=base_url)
833def test_overwrite_retry_count_header(
834self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
835) -> None:
836client = client.with_options(max_retries=4)
837
838nb_retries = 0
839
840def retry_handler(_request: httpx.Request) -> httpx.Response:
841nonlocal nb_retries
842if nb_retries < failures_before_success:
843nb_retries += 1
844return httpx.Response(500)
845return httpx.Response(200)
846
847respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
848
849response = client.chat.completions.with_raw_response.create(
850messages=[
851{
852"content": "string",
575ff607stainless-app[bot]1 years ago853"role": "developer",
5449e208Stainless Bot1 years ago854}
855],
856model="gpt-4o",
857extra_headers={"x-stainless-retry-count": "42"},
858)
859
860assert response.http_request.headers.get("x-stainless-retry-count") == "42"
98d8b2acstainless-app[bot]1 years ago861
862@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
863@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
864@pytest.mark.respx(base_url=base_url)
865def test_retries_taken_new_response_class(
866self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
867) -> None:
868client = client.with_options(max_retries=4)
869
870nb_retries = 0
871
872def retry_handler(_request: httpx.Request) -> httpx.Response:
873nonlocal nb_retries
874if nb_retries < failures_before_success:
875nb_retries += 1
876return httpx.Response(500)
877return httpx.Response(200)
878
879respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
880
881with client.chat.completions.with_streaming_response.create(
882messages=[
883{
bf1ca86cRobert Craigie1 years ago884"content": "string",
575ff607stainless-app[bot]1 years ago885"role": "developer",
98d8b2acstainless-app[bot]1 years ago886}
887],
bf1ca86cRobert Craigie1 years ago888model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago889) as response:
890assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago891assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
98d8b2acstainless-app[bot]1 years ago892
cc2c1fc1stainless-app[bot]1 years ago893def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
894# Test that the proxy environment variables are set correctly
895monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
896
897client = DefaultHttpxClient()
898
899mounts = tuple(client._mounts.items())
900assert len(mounts) == 1
901assert mounts[0][0].pattern == "https://"
902
903@pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
904def test_default_client_creation(self) -> None:
905# Ensure that the client can be initialized without any exceptions
906DefaultHttpxClient(
907verify=True,
908cert=None,
909trust_env=True,
910http1=True,
911http2=False,
912limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
913)
914
cca09707stainless-app[bot]1 years ago915@pytest.mark.respx(base_url=base_url)
916def test_follow_redirects(self, respx_mock: MockRouter) -> None:
917# Test that the default follow_redirects=True allows following redirects
918respx_mock.post("/redirect").mock(
919return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
920)
921respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
922
923response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
924assert response.status_code == 200
925assert response.json() == {"status": "ok"}
926
927@pytest.mark.respx(base_url=base_url)
928def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
929# Test that follow_redirects=False prevents following redirects
930respx_mock.post("/redirect").mock(
931return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
932)
933
934with pytest.raises(APIStatusError) as exc_info:
935self.client.post(
936"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
937)
938
939assert exc_info.value.response.status_code == 302
940assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
941
08b8179aDavid Schnurr2 years ago942
943class TestAsyncOpenAI:
944client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
945
946@pytest.mark.respx(base_url=base_url)
947@pytest.mark.asyncio
948async def test_raw_response(self, respx_mock: MockRouter) -> None:
c5975bd0Stainless Bot2 years ago949respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago950
951response = await self.client.post("/foo", cast_to=httpx.Response)
952assert response.status_code == 200
953assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago954assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago955
956@pytest.mark.respx(base_url=base_url)
957@pytest.mark.asyncio
958async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
959respx_mock.post("/foo").mock(
960return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
961)
962
963response = await self.client.post("/foo", cast_to=httpx.Response)
964assert response.status_code == 200
965assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago966assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago967
968def test_copy(self) -> None:
969copied = self.client.copy()
970assert id(copied) != id(self.client)
971
972copied = self.client.copy(api_key="another My API Key")
973assert copied.api_key == "another My API Key"
974assert self.client.api_key == "My API Key"
975
976def test_copy_default_options(self) -> None:
977# options that have a default are overridden correctly
978copied = self.client.copy(max_retries=7)
979assert copied.max_retries == 7
980assert self.client.max_retries == 2
981
982copied2 = copied.copy(max_retries=6)
983assert copied2.max_retries == 6
984assert copied.max_retries == 7
985
986# timeout
987assert isinstance(self.client.timeout, httpx.Timeout)
988copied = self.client.copy(timeout=None)
989assert copied.timeout is None
990assert isinstance(self.client.timeout, httpx.Timeout)
991
992def test_copy_default_headers(self) -> None:
993client = AsyncOpenAI(
994base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
995)
996assert client.default_headers["X-Foo"] == "bar"
997
998# does not override the already given value when not specified
999copied = client.copy()
1000assert copied.default_headers["X-Foo"] == "bar"
1001
1002# merges already given headers
1003copied = client.copy(default_headers={"X-Bar": "stainless"})
1004assert copied.default_headers["X-Foo"] == "bar"
1005assert copied.default_headers["X-Bar"] == "stainless"
1006
1007# uses new values for any already given headers
1008copied = client.copy(default_headers={"X-Foo": "stainless"})
1009assert copied.default_headers["X-Foo"] == "stainless"
1010
1011# set_default_headers
1012
1013# completely overrides already set values
1014copied = client.copy(set_default_headers={})
1015assert copied.default_headers.get("X-Foo") is None
1016
1017copied = client.copy(set_default_headers={"X-Bar": "Robert"})
1018assert copied.default_headers["X-Bar"] == "Robert"
1019
1020with pytest.raises(
1021ValueError,
1022match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
1023):
1024client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
1025
1026def test_copy_default_query(self) -> None:
1027client = AsyncOpenAI(
1028base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
1029)
1030assert _get_params(client)["foo"] == "bar"
1031
1032# does not override the already given value when not specified
1033copied = client.copy()
1034assert _get_params(copied)["foo"] == "bar"
1035
1036# merges already given params
1037copied = client.copy(default_query={"bar": "stainless"})
1038params = _get_params(copied)
1039assert params["foo"] == "bar"
1040assert params["bar"] == "stainless"
1041
1042# uses new values for any already given headers
1043copied = client.copy(default_query={"foo": "stainless"})
1044assert _get_params(copied)["foo"] == "stainless"
1045
1046# set_default_query
1047
1048# completely overrides already set values
1049copied = client.copy(set_default_query={})
1050assert _get_params(copied) == {}
1051
1052copied = client.copy(set_default_query={"bar": "Robert"})
1053assert _get_params(copied)["bar"] == "Robert"
1054
1055with pytest.raises(
1056ValueError,
1057# TODO: update
1058match="`default_query` and `set_default_query` arguments are mutually exclusive",
1059):
1060client.copy(set_default_query={}, default_query={"foo": "Bar"})
1061
1062def test_copy_signature(self) -> None:
1063# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
1064init_signature = inspect.signature(
1065# mypy doesn't like that we access the `__init__` property.
1066self.client.__init__, # type: ignore[misc]
1067)
1068copy_signature = inspect.signature(self.client.copy)
1069exclude_params = {"transport", "proxies", "_strict_response_validation"}
1070
1071for name in init_signature.parameters.keys():
1072if name in exclude_params:
1073continue
1074
1075copy_param = copy_signature.parameters.get(name)
1076assert copy_param is not None, f"copy() signature is missing the {name} param"
1077
18e0b36astainless-app[bot]1 years ago1078@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
d052708aStainless Bot2 years ago1079def test_copy_build_request(self) -> None:
1080options = FinalRequestOptions(method="get", url="/foo")
1081
1082def build_request(options: FinalRequestOptions) -> None:
1083client = self.client.copy()
1084client._build_request(options)
1085
1086# ensure that the machinery is warmed up before tracing starts.
1087build_request(options)
1088gc.collect()
1089
1090tracemalloc.start(1000)
1091
1092snapshot_before = tracemalloc.take_snapshot()
1093
1094ITERATIONS = 10
1095for _ in range(ITERATIONS):
1096build_request(options)
1097
ce04ec28Stainless Bot2 years ago1098gc.collect()
d052708aStainless Bot2 years ago1099snapshot_after = tracemalloc.take_snapshot()
1100
1101tracemalloc.stop()
1102
1103def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
1104if diff.count == 0:
1105# Avoid false positives by considering only leaks (i.e. allocations that persist).
1106return
1107
1108if diff.count % ITERATIONS != 0:
1109# Avoid false positives by considering only leaks that appear per iteration.
1110return
1111
1112for frame in diff.traceback:
1113if any(
1114frame.filename.endswith(fragment)
1115for fragment in [
1116# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
1117#
1118# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago1119"openai/_legacy_response.py",
d052708aStainless Bot2 years ago1120"openai/_response.py",
1121# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
1122"openai/_compat.py",
1123# Standard library leaks we don't care about.
1124"/logging/__init__.py",
1125]
1126):
1127return
1128
1129leaks.append(diff)
1130
1131leaks: list[tracemalloc.StatisticDiff] = []
1132for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
1133add_leak(leaks, diff)
1134if leaks:
1135for leak in leaks:
1136print("MEMORY LEAK:", leak)
1137for frame in leak.traceback:
1138print(frame)
1139raise AssertionError()
1140
08b8179aDavid Schnurr2 years ago1141async def test_request_timeout(self) -> None:
1142request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
1143timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1144assert timeout == DEFAULT_TIMEOUT
1145
1146request = self.client._build_request(
1147FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
1148)
1149timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1150assert timeout == httpx.Timeout(100.0)
1151
1152async def test_client_timeout_option(self) -> None:
1153client = AsyncOpenAI(
1154base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0)
1155)
1156
1157request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1158timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1159assert timeout == httpx.Timeout(0)
1160
1161async def test_http_client_timeout_option(self) -> None:
1162# custom timeout given to the httpx client should be used
1163async with httpx.AsyncClient(timeout=None) as http_client:
1164client = AsyncOpenAI(
1165base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1166)
1167
1168request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1169timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1170assert timeout == httpx.Timeout(None)
1171
1172# no timeout given to the httpx client should not use the httpx default
1173async with httpx.AsyncClient() as http_client:
1174client = AsyncOpenAI(
1175base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1176)
1177
1178request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1179timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1180assert timeout == DEFAULT_TIMEOUT
1181
1182# explicitly passing the default timeout currently results in it being ignored
1183async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
1184client = AsyncOpenAI(
1185base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1186)
1187
1188request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1189timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1190assert timeout == DEFAULT_TIMEOUT # our default
1191
dae0ec80Stainless Bot2 years ago1192def test_invalid_http_client(self) -> None:
1193with pytest.raises(TypeError, match="Invalid `http_client` arg"):
1194with httpx.Client() as http_client:
1195AsyncOpenAI(
1196base_url=base_url,
1197api_key=api_key,
1198_strict_response_validation=True,
1199http_client=cast(Any, http_client),
1200)
1201
08b8179aDavid Schnurr2 years ago1202def test_default_headers_option(self) -> None:
1203client = AsyncOpenAI(
1204base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
1205)
1206request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1207assert request.headers.get("x-foo") == "bar"
1208assert request.headers.get("x-stainless-lang") == "python"
1209
1210client2 = AsyncOpenAI(
1211base_url=base_url,
1212api_key=api_key,
1213_strict_response_validation=True,
1214default_headers={
1215"X-Foo": "stainless",
1216"X-Stainless-Lang": "my-overriding-header",
1217},
1218)
1219request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
1220assert request.headers.get("x-foo") == "stainless"
1221assert request.headers.get("x-stainless-lang") == "my-overriding-header"
1222
1223def test_validate_headers(self) -> None:
1224client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1225request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1226assert request.headers.get("Authorization") == f"Bearer {api_key}"
1227
e967f5a5Stainless Bot2 years ago1228with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago1229with update_env(**{"OPENAI_API_KEY": Omit()}):
1230client2 = AsyncOpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago1231_ = client2
1232
1233def test_default_query_option(self) -> None:
1234client = AsyncOpenAI(
1235base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
1236)
1237request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1238url = httpx.URL(request.url)
1239assert dict(url.params) == {"query_param": "bar"}
1240
1241request = client._build_request(
1242FinalRequestOptions(
1243method="get",
1244url="/foo",
eba67815stainless-app[bot]1 years ago1245params={"foo": "baz", "query_param": "overridden"},
08b8179aDavid Schnurr2 years ago1246)
1247)
1248url = httpx.URL(request.url)
eba67815stainless-app[bot]1 years ago1249assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
08b8179aDavid Schnurr2 years ago1250
1251def test_request_extra_json(self) -> None:
1252request = self.client._build_request(
1253FinalRequestOptions(
1254method="post",
1255url="/foo",
1256json_data={"foo": "bar"},
1257extra_json={"baz": False},
1258),
1259)
1260data = json.loads(request.content.decode("utf-8"))
1261assert data == {"foo": "bar", "baz": False}
1262
1263request = self.client._build_request(
1264FinalRequestOptions(
1265method="post",
1266url="/foo",
1267extra_json={"baz": False},
1268),
1269)
1270data = json.loads(request.content.decode("utf-8"))
1271assert data == {"baz": False}
1272
1273# `extra_json` takes priority over `json_data` when keys clash
1274request = self.client._build_request(
1275FinalRequestOptions(
1276method="post",
1277url="/foo",
1278json_data={"foo": "bar", "baz": True},
1279extra_json={"baz": None},
1280),
1281)
1282data = json.loads(request.content.decode("utf-8"))
1283assert data == {"foo": "bar", "baz": None}
1284
1285def test_request_extra_headers(self) -> None:
1286request = self.client._build_request(
1287FinalRequestOptions(
1288method="post",
1289url="/foo",
1290**make_request_options(extra_headers={"X-Foo": "Foo"}),
1291),
1292)
1293assert request.headers.get("X-Foo") == "Foo"
1294
1295# `extra_headers` takes priority over `default_headers` when keys clash
1296request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
1297FinalRequestOptions(
1298method="post",
1299url="/foo",
1300**make_request_options(
1301extra_headers={"X-Bar": "false"},
1302),
1303),
1304)
1305assert request.headers.get("X-Bar") == "false"
1306
1307def test_request_extra_query(self) -> None:
1308request = self.client._build_request(
1309FinalRequestOptions(
1310method="post",
1311url="/foo",
1312**make_request_options(
1313extra_query={"my_query_param": "Foo"},
1314),
1315),
1316)
31573844Stainless Bot2 years ago1317params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1318assert params == {"my_query_param": "Foo"}
1319
1320# if both `query` and `extra_query` are given, they are merged
1321request = self.client._build_request(
1322FinalRequestOptions(
1323method="post",
1324url="/foo",
1325**make_request_options(
1326query={"bar": "1"},
1327extra_query={"foo": "2"},
1328),
1329),
1330)
31573844Stainless Bot2 years ago1331params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1332assert params == {"bar": "1", "foo": "2"}
1333
1334# `extra_query` takes priority over `query` when keys clash
1335request = self.client._build_request(
1336FinalRequestOptions(
1337method="post",
1338url="/foo",
1339**make_request_options(
1340query={"foo": "1"},
1341extra_query={"foo": "2"},
1342),
1343),
1344)
31573844Stainless Bot2 years ago1345params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1346assert params == {"foo": "2"}
1347
22713fd0Stainless Bot2 years ago1348def test_multipart_repeating_array(self, async_client: AsyncOpenAI) -> None:
1349request = async_client._build_request(
1350FinalRequestOptions.construct(
fcbb5983stainless-app[bot]11 months ago1351method="post",
22713fd0Stainless Bot2 years ago1352url="/foo",
1353headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
1354json_data={"array": ["foo", "bar"]},
1355files=[("foo.txt", b"hello world")],
1356)
1357)
1358
1359assert request.read().split(b"\r\n") == [
1360b"--6b7ba517decee4a450543ea6ae821c82",
1361b'Content-Disposition: form-data; name="array[]"',
1362b"",
1363b"foo",
1364b"--6b7ba517decee4a450543ea6ae821c82",
1365b'Content-Disposition: form-data; name="array[]"',
1366b"",
1367b"bar",
1368b"--6b7ba517decee4a450543ea6ae821c82",
1369b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
1370b"Content-Type: application/octet-stream",
1371b"",
1372b"hello world",
1373b"--6b7ba517decee4a450543ea6ae821c82--",
1374b"",
1375]
1376
08b8179aDavid Schnurr2 years ago1377@pytest.mark.respx(base_url=base_url)
1378async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
1379class Model1(BaseModel):
1380name: str
1381
1382class Model2(BaseModel):
1383foo: str
1384
1385respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1386
1387response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1388assert isinstance(response, Model2)
1389assert response.foo == "bar"
1390
1391@pytest.mark.respx(base_url=base_url)
1392async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
1393"""Union of objects with the same field name using a different type"""
1394
1395class Model1(BaseModel):
1396foo: int
1397
1398class Model2(BaseModel):
1399foo: str
1400
1401respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1402
1403response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1404assert isinstance(response, Model2)
1405assert response.foo == "bar"
1406
1407respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
1408
1409response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1410assert isinstance(response, Model1)
1411assert response.foo == 1
1412
c26014e2Stainless Bot2 years ago1413@pytest.mark.respx(base_url=base_url)
1414async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
1415"""
1416Response that sets Content-Type to something other than application/json but returns json data
1417"""
1418
1419class Model(BaseModel):
1420foo: int
1421
1422respx_mock.get("/foo").mock(
1423return_value=httpx.Response(
1424200,
1425content=json.dumps({"foo": 2}),
1426headers={"Content-Type": "application/text"},
1427)
1428)
1429
1430response = await self.client.get("/foo", cast_to=Model)
1431assert isinstance(response, Model)
1432assert response.foo == 2
1433
f6f38a9bStainless Bot2 years ago1434def test_base_url_setter(self) -> None:
1435client = AsyncOpenAI(
1436base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True
1437)
1438assert client.base_url == "https://example.com/from_init/"
1439
1440client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
1441
1442assert client.base_url == "https://example.com/from_setter/"
1443
0733934fStainless Bot2 years ago1444def test_base_url_env(self) -> None:
1445with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
1446client = AsyncOpenAI(api_key=api_key, _strict_response_validation=True)
1447assert client.base_url == "http://localhost:5000/from/env/"
1448
08b8179aDavid Schnurr2 years ago1449@pytest.mark.parametrize(
1450"client",
1451[
1452AsyncOpenAI(
1453base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1454),
1455AsyncOpenAI(
1456base_url="http://localhost:5000/custom/path/",
1457api_key=api_key,
1458_strict_response_validation=True,
1459http_client=httpx.AsyncClient(),
1460),
1461],
1462ids=["standard", "custom http client"],
1463)
1464def test_base_url_trailing_slash(self, client: AsyncOpenAI) -> None:
1465request = client._build_request(
1466FinalRequestOptions(
1467method="post",
1468url="/foo",
1469json_data={"foo": "bar"},
1470),
1471)
1472assert request.url == "http://localhost:5000/custom/path/foo"
1473
1474@pytest.mark.parametrize(
1475"client",
1476[
1477AsyncOpenAI(
1478base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1479),
1480AsyncOpenAI(
1481base_url="http://localhost:5000/custom/path/",
1482api_key=api_key,
1483_strict_response_validation=True,
1484http_client=httpx.AsyncClient(),
1485),
1486],
1487ids=["standard", "custom http client"],
1488)
1489def test_base_url_no_trailing_slash(self, client: AsyncOpenAI) -> None:
1490request = client._build_request(
1491FinalRequestOptions(
1492method="post",
1493url="/foo",
1494json_data={"foo": "bar"},
1495),
1496)
1497assert request.url == "http://localhost:5000/custom/path/foo"
1498
1499@pytest.mark.parametrize(
1500"client",
1501[
1502AsyncOpenAI(
1503base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1504),
1505AsyncOpenAI(
1506base_url="http://localhost:5000/custom/path/",
1507api_key=api_key,
1508_strict_response_validation=True,
1509http_client=httpx.AsyncClient(),
1510),
1511],
1512ids=["standard", "custom http client"],
1513)
1514def test_absolute_request_url(self, client: AsyncOpenAI) -> None:
1515request = client._build_request(
1516FinalRequestOptions(
1517method="post",
1518url="https://myapi.com/foo",
1519json_data={"foo": "bar"},
1520),
1521)
1522assert request.url == "https://myapi.com/foo"
1523
1524async def test_copied_client_does_not_close_http(self) -> None:
1525client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1526assert not client.is_closed()
1527
1528copied = client.copy()
1529assert copied is not client
1530
a7ebc260Stainless Bot2 years ago1531del copied
08b8179aDavid Schnurr2 years ago1532
1533await asyncio.sleep(0.2)
1534assert not client.is_closed()
1535
1536async def test_client_context_manager(self) -> None:
1537client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1538async with client as c2:
1539assert c2 is client
1540assert not c2.is_closed()
1541assert not client.is_closed()
1542assert client.is_closed()
1543
1544@pytest.mark.respx(base_url=base_url)
1545@pytest.mark.asyncio
1546async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
1547class Model(BaseModel):
1548foo: str
1549
1550respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
1551
1552with pytest.raises(APIResponseValidationError) as exc:
1553await self.client.get("/foo", cast_to=Model)
1554
1555assert isinstance(exc.value.__cause__, ValidationError)
1556
07079085Stainless Bot2 years ago1557async def test_client_max_retries_validation(self) -> None:
1558with pytest.raises(TypeError, match=r"max_retries cannot be None"):
1559AsyncOpenAI(
1560base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)
1561)
1562
08b8179aDavid Schnurr2 years ago1563@pytest.mark.respx(base_url=base_url)
1564@pytest.mark.asyncio
1565async def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
1566class Model(BaseModel):
1567name: str
1568
1569respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1570
86379b44Stainless Bot2 years ago1571stream = await self.client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
1572assert isinstance(stream, AsyncStream)
1573await stream.response.aclose()
08b8179aDavid Schnurr2 years ago1574
1575@pytest.mark.respx(base_url=base_url)
1576@pytest.mark.asyncio
1577async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
1578class Model(BaseModel):
1579name: str
1580
1581respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
1582
1583strict_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1584
1585with pytest.raises(APIResponseValidationError):
1586await strict_client.get("/foo", cast_to=Model)
1587
1588client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
1589
1590response = await client.get("/foo", cast_to=Model)
1591assert isinstance(response, str) # type: ignore[unreachable]
1592
1593@pytest.mark.parametrize(
1594"remaining_retries,retry_after,timeout",
1595[
1596[3, "20", 20],
1597[3, "0", 0.5],
1598[3, "-10", 0.5],
1599[3, "60", 60],
1600[3, "61", 0.5],
1601[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
1602[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
1603[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
1604[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
1605[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
1606[3, "99999999999999999999999999999999999", 0.5],
1607[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
1608[3, "", 0.5],
1609[2, "", 0.5 * 2.0],
1610[1, "", 0.5 * 4.0],
7f6a921cstainless-app[bot]1 years ago1611[-1100, "", 8], # test large number potentially overflowing
08b8179aDavid Schnurr2 years ago1612],
1613)
1614@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
1615@pytest.mark.asyncio
1616async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
1617client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1618
1619headers = httpx.Headers({"retry-after": retry_after})
1620options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
1621calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
1622assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
7aad3405Stainless Bot2 years ago1623
ba4f7a97Stainless Bot2 years ago1624@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1625@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago1626async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
ba4f7a97Stainless Bot2 years ago1627respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
1628
1629with pytest.raises(APITimeoutError):
0bef1d02stainless-app[bot]1 years ago1630await async_client.chat.completions.with_streaming_response.create(
1631messages=[
1632{
1633"content": "string",
1634"role": "developer",
1635}
1636],
1637model="gpt-4o",
1638).__aenter__()
7aad3405Stainless Bot2 years ago1639
1640assert _get_open_connections(self.client) == 0
1641
ba4f7a97Stainless Bot2 years ago1642@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago1643@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago1644async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
ba4f7a97Stainless Bot2 years ago1645respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago1646
ba4f7a97Stainless Bot2 years ago1647with pytest.raises(APIStatusError):
0bef1d02stainless-app[bot]1 years ago1648await async_client.chat.completions.with_streaming_response.create(
1649messages=[
1650{
1651"content": "string",
1652"role": "developer",
1653}
1654],
1655model="gpt-4o",
1656).__aenter__()
ba4f7a97Stainless Bot2 years ago1657assert _get_open_connections(self.client) == 0
98d8b2acstainless-app[bot]1 years ago1658
1659@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1660@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1661@pytest.mark.respx(base_url=base_url)
1662@pytest.mark.asyncio
6634f525stainless-app[bot]1 years ago1663@pytest.mark.parametrize("failure_mode", ["status", "exception"])
98d8b2acstainless-app[bot]1 years ago1664async def test_retries_taken(
6634f525stainless-app[bot]1 years ago1665self,
1666async_client: AsyncOpenAI,
1667failures_before_success: int,
1668failure_mode: Literal["status", "exception"],
1669respx_mock: MockRouter,
98d8b2acstainless-app[bot]1 years ago1670) -> None:
1671client = async_client.with_options(max_retries=4)
1672
1673nb_retries = 0
1674
1675def retry_handler(_request: httpx.Request) -> httpx.Response:
1676nonlocal nb_retries
1677if nb_retries < failures_before_success:
1678nb_retries += 1
6634f525stainless-app[bot]1 years ago1679if failure_mode == "exception":
1680raise RuntimeError("oops")
98d8b2acstainless-app[bot]1 years ago1681return httpx.Response(500)
1682return httpx.Response(200)
1683
1684respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1685
1686response = await client.chat.completions.with_raw_response.create(
1687messages=[
1688{
bf1ca86cRobert Craigie1 years ago1689"content": "string",
575ff607stainless-app[bot]1 years ago1690"role": "developer",
98d8b2acstainless-app[bot]1 years ago1691}
1692],
bf1ca86cRobert Craigie1 years ago1693model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago1694)
1695
1696assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago1697assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
1698
1699@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1700@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1701@pytest.mark.respx(base_url=base_url)
1702@pytest.mark.asyncio
1703async def test_omit_retry_count_header(
1704self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1705) -> None:
1706client = async_client.with_options(max_retries=4)
1707
1708nb_retries = 0
1709
1710def retry_handler(_request: httpx.Request) -> httpx.Response:
1711nonlocal nb_retries
1712if nb_retries < failures_before_success:
1713nb_retries += 1
1714return httpx.Response(500)
1715return httpx.Response(200)
1716
1717respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1718
1719response = await client.chat.completions.with_raw_response.create(
1720messages=[
1721{
1722"content": "string",
575ff607stainless-app[bot]1 years ago1723"role": "developer",
5449e208Stainless Bot1 years ago1724}
1725],
1726model="gpt-4o",
1727extra_headers={"x-stainless-retry-count": Omit()},
1728)
1729
1730assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
1731
1732@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1733@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1734@pytest.mark.respx(base_url=base_url)
1735@pytest.mark.asyncio
1736async def test_overwrite_retry_count_header(
1737self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1738) -> None:
1739client = async_client.with_options(max_retries=4)
1740
1741nb_retries = 0
1742
1743def retry_handler(_request: httpx.Request) -> httpx.Response:
1744nonlocal nb_retries
1745if nb_retries < failures_before_success:
1746nb_retries += 1
1747return httpx.Response(500)
1748return httpx.Response(200)
1749
1750respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1751
1752response = await client.chat.completions.with_raw_response.create(
1753messages=[
1754{
1755"content": "string",
575ff607stainless-app[bot]1 years ago1756"role": "developer",
5449e208Stainless Bot1 years ago1757}
1758],
1759model="gpt-4o",
1760extra_headers={"x-stainless-retry-count": "42"},
1761)
1762
1763assert response.http_request.headers.get("x-stainless-retry-count") == "42"
98d8b2acstainless-app[bot]1 years ago1764
1765@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1766@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1767@pytest.mark.respx(base_url=base_url)
1768@pytest.mark.asyncio
1769async def test_retries_taken_new_response_class(
1770self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1771) -> None:
1772client = async_client.with_options(max_retries=4)
1773
1774nb_retries = 0
1775
1776def retry_handler(_request: httpx.Request) -> httpx.Response:
1777nonlocal nb_retries
1778if nb_retries < failures_before_success:
1779nb_retries += 1
1780return httpx.Response(500)
1781return httpx.Response(200)
1782
1783respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1784
1785async with client.chat.completions.with_streaming_response.create(
1786messages=[
1787{
bf1ca86cRobert Craigie1 years ago1788"content": "string",
575ff607stainless-app[bot]1 years ago1789"role": "developer",
98d8b2acstainless-app[bot]1 years ago1790}
1791],
bf1ca86cRobert Craigie1 years ago1792model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago1793) as response:
1794assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago1795assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
d8901d28Seth Gilchrist1 years ago1796
1797def test_get_platform(self) -> None:
53ab0467Stainless Bot1 years ago1798# A previous implementation of asyncify could leave threads unterminated when
1799# used with nest_asyncio.
1800#
d8901d28Seth Gilchrist1 years ago1801# Since nest_asyncio.apply() is global and cannot be un-applied, this
1802# test is run in a separate process to avoid affecting other tests.
53ab0467Stainless Bot1 years ago1803test_code = dedent("""
d8901d28Seth Gilchrist1 years ago1804import asyncio
1805import nest_asyncio
1806import threading
1807
1808from openai._utils import asyncify
fb69e674stainless-app[bot]1 years ago1809from openai._base_client import get_platform
d8901d28Seth Gilchrist1 years ago1810
1811async def test_main() -> None:
1812result = await asyncify(get_platform)()
1813print(result)
1814for thread in threading.enumerate():
1815print(thread.name)
1816
1817nest_asyncio.apply()
1818asyncio.run(test_main())
1819""")
1820with subprocess.Popen(
1821[sys.executable, "-c", test_code],
1822text=True,
1823) as process:
14543c59stainless-app[bot]1 years ago1824timeout = 10 # seconds
1825
1826start_time = time.monotonic()
1827while True:
1828return_code = process.poll()
1829if return_code is not None:
1830if return_code != 0:
1831raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
1832
1833# success
1834break
1835
1836if time.monotonic() - start_time > timeout:
1837process.kill()
1838raise AssertionError("calling get_platform using asyncify resulted in a hung process")
1839
1840time.sleep(0.1)
cca09707stainless-app[bot]1 years ago1841
cc2c1fc1stainless-app[bot]1 years ago1842async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
1843# Test that the proxy environment variables are set correctly
1844monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
1845
1846client = DefaultAsyncHttpxClient()
1847
1848mounts = tuple(client._mounts.items())
1849assert len(mounts) == 1
1850assert mounts[0][0].pattern == "https://"
1851
1852@pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
1853async def test_default_client_creation(self) -> None:
1854# Ensure that the client can be initialized without any exceptions
1855DefaultAsyncHttpxClient(
1856verify=True,
1857cert=None,
1858trust_env=True,
1859http1=True,
1860http2=False,
1861limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
1862)
1863
cca09707stainless-app[bot]1 years ago1864@pytest.mark.respx(base_url=base_url)
1865async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
1866# Test that the default follow_redirects=True allows following redirects
1867respx_mock.post("/redirect").mock(
1868return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1869)
1870respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
1871
1872response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
1873assert response.status_code == 200
1874assert response.json() == {"status": "ok"}
1875
1876@pytest.mark.respx(base_url=base_url)
1877async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
1878# Test that follow_redirects=False prevents following redirects
1879respx_mock.post("/redirect").mock(
1880return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1881)
1882
1883with pytest.raises(APIStatusError) as exc_info:
1884await self.client.post(
1885"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
1886)
1887
1888assert exc_info.value.response.status_code == 302
1889assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"