Skip to content

Commit c3eee55

Browse files
feat(api): update via SDK Studio
1 parent ffbe827 commit c3eee55

File tree

5 files changed

+196
-16
lines changed

5 files changed

+196
-16
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ The full API of this library can be found in [api.md](api.md).
2929
```python
3030
from codex import Codex
3131

32-
client = Codex()
32+
client = Codex(
33+
# or 'production' | 'local'; defaults to "production".
34+
environment="staging",
35+
)
3336

3437
project_return_schema = client.projects.create(
3538
config={},
@@ -52,7 +55,10 @@ Simply import `AsyncCodex` instead of `Codex` and use `await` with each API call
5255
import asyncio
5356
from codex import AsyncCodex
5457

55-
client = AsyncCodex()
58+
client = AsyncCodex(
59+
# or 'production' | 'local'; defaults to "production".
60+
environment="staging",
61+
)
5662

5763

5864
async def main() -> None:

src/codex/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,18 @@
33
from . import types
44
from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes
55
from ._utils import file_from_path
6-
from ._client import Codex, Client, Stream, Timeout, Transport, AsyncCodex, AsyncClient, AsyncStream, RequestOptions
6+
from ._client import (
7+
ENVIRONMENTS,
8+
Codex,
9+
Client,
10+
Stream,
11+
Timeout,
12+
Transport,
13+
AsyncCodex,
14+
AsyncClient,
15+
AsyncStream,
16+
RequestOptions,
17+
)
718
from ._models import BaseModel
819
from ._version import __title__, __version__
920
from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse
@@ -59,6 +70,7 @@
5970
"AsyncStream",
6071
"Codex",
6172
"AsyncCodex",
73+
"ENVIRONMENTS",
6274
"file_from_path",
6375
"BaseModel",
6476
"DEFAULT_TIMEOUT",

src/codex/_client.py

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from __future__ import annotations
44

55
import os
6-
from typing import Any, Union, Mapping
7-
from typing_extensions import Self, override
6+
from typing import Any, Dict, Union, Mapping, cast
7+
from typing_extensions import Self, Literal, override
88

99
import httpx
1010

@@ -37,7 +37,23 @@
3737
from .resources.projects import projects
3838
from .resources.organizations import organizations
3939

40-
__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Codex", "AsyncCodex", "Client", "AsyncClient"]
40+
__all__ = [
41+
"ENVIRONMENTS",
42+
"Timeout",
43+
"Transport",
44+
"ProxiesTypes",
45+
"RequestOptions",
46+
"Codex",
47+
"AsyncCodex",
48+
"Client",
49+
"AsyncClient",
50+
]
51+
52+
ENVIRONMENTS: Dict[str, str] = {
53+
"production": "https://api-alpha-o3gxj3oajfu.cleanlab.ai",
54+
"staging": "https://api-alpha-staging-o3gxj3oajfu.cleanlab.ai",
55+
"local": "http://localhost:8080",
56+
}
4157

4258

4359
class Codex(SyncAPIClient):
@@ -53,13 +69,16 @@ class Codex(SyncAPIClient):
5369
api_key: str | None
5470
access_key: str | None
5571

72+
_environment: Literal["production", "staging", "local"] | NotGiven
73+
5674
def __init__(
5775
self,
5876
*,
5977
bearer_token: str | None = None,
6078
api_key: str | None = None,
6179
access_key: str | None = None,
62-
base_url: str | httpx.URL | None = None,
80+
environment: Literal["production", "staging", "local"] | NotGiven = NOT_GIVEN,
81+
base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN,
6382
timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
6483
max_retries: int = DEFAULT_MAX_RETRIES,
6584
default_headers: Mapping[str, str] | None = None,
@@ -97,10 +116,31 @@ def __init__(
97116
access_key = os.environ.get("PUBLIC_ACCESS_KEY")
98117
self.access_key = access_key
99118

100-
if base_url is None:
101-
base_url = os.environ.get("CODEX_BASE_URL")
102-
if base_url is None:
103-
base_url = f"https://localhost:8080/test-api"
119+
self._environment = environment
120+
121+
base_url_env = os.environ.get("CODEX_BASE_URL")
122+
if is_given(base_url) and base_url is not None:
123+
# cast required because mypy doesn't understand the type narrowing
124+
base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast]
125+
elif is_given(environment):
126+
if base_url_env and base_url is not None:
127+
raise ValueError(
128+
"Ambiguous URL; The `CODEX_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None",
129+
)
130+
131+
try:
132+
base_url = ENVIRONMENTS[environment]
133+
except KeyError as exc:
134+
raise ValueError(f"Unknown environment: {environment}") from exc
135+
elif base_url_env is not None:
136+
base_url = base_url_env
137+
else:
138+
self._environment = environment = "production"
139+
140+
try:
141+
base_url = ENVIRONMENTS[environment]
142+
except KeyError as exc:
143+
raise ValueError(f"Unknown environment: {environment}") from exc
104144

105145
super().__init__(
106146
version=__version__,
@@ -193,6 +233,7 @@ def copy(
193233
bearer_token: str | None = None,
194234
api_key: str | None = None,
195235
access_key: str | None = None,
236+
environment: Literal["production", "staging", "local"] | None = None,
196237
base_url: str | httpx.URL | None = None,
197238
timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
198239
http_client: httpx.Client | None = None,
@@ -230,6 +271,7 @@ def copy(
230271
api_key=api_key or self.api_key,
231272
access_key=access_key or self.access_key,
232273
base_url=base_url or self.base_url,
274+
environment=environment or self._environment,
233275
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
234276
http_client=http_client,
235277
max_retries=max_retries if is_given(max_retries) else self.max_retries,
@@ -289,13 +331,16 @@ class AsyncCodex(AsyncAPIClient):
289331
api_key: str | None
290332
access_key: str | None
291333

334+
_environment: Literal["production", "staging", "local"] | NotGiven
335+
292336
def __init__(
293337
self,
294338
*,
295339
bearer_token: str | None = None,
296340
api_key: str | None = None,
297341
access_key: str | None = None,
298-
base_url: str | httpx.URL | None = None,
342+
environment: Literal["production", "staging", "local"] | NotGiven = NOT_GIVEN,
343+
base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN,
299344
timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
300345
max_retries: int = DEFAULT_MAX_RETRIES,
301346
default_headers: Mapping[str, str] | None = None,
@@ -333,10 +378,31 @@ def __init__(
333378
access_key = os.environ.get("PUBLIC_ACCESS_KEY")
334379
self.access_key = access_key
335380

336-
if base_url is None:
337-
base_url = os.environ.get("CODEX_BASE_URL")
338-
if base_url is None:
339-
base_url = f"https://localhost:8080/test-api"
381+
self._environment = environment
382+
383+
base_url_env = os.environ.get("CODEX_BASE_URL")
384+
if is_given(base_url) and base_url is not None:
385+
# cast required because mypy doesn't understand the type narrowing
386+
base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast]
387+
elif is_given(environment):
388+
if base_url_env and base_url is not None:
389+
raise ValueError(
390+
"Ambiguous URL; The `CODEX_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None",
391+
)
392+
393+
try:
394+
base_url = ENVIRONMENTS[environment]
395+
except KeyError as exc:
396+
raise ValueError(f"Unknown environment: {environment}") from exc
397+
elif base_url_env is not None:
398+
base_url = base_url_env
399+
else:
400+
self._environment = environment = "production"
401+
402+
try:
403+
base_url = ENVIRONMENTS[environment]
404+
except KeyError as exc:
405+
raise ValueError(f"Unknown environment: {environment}") from exc
340406

341407
super().__init__(
342408
version=__version__,
@@ -429,6 +495,7 @@ def copy(
429495
bearer_token: str | None = None,
430496
api_key: str | None = None,
431497
access_key: str | None = None,
498+
environment: Literal["production", "staging", "local"] | None = None,
432499
base_url: str | httpx.URL | None = None,
433500
timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
434501
http_client: httpx.AsyncClient | None = None,
@@ -466,6 +533,7 @@ def copy(
466533
api_key=api_key or self.api_key,
467534
access_key=access_key or self.access_key,
468535
base_url=base_url or self.base_url,
536+
environment=environment or self._environment,
469537
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
470538
http_client=http_client,
471539
max_retries=max_retries if is_given(max_retries) else self.max_retries,

src/codex/pagination.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
from typing import Any, List, Type, Generic, Mapping, TypeVar, Optional, cast
4+
from typing_extensions import override
5+
6+
from httpx import Response
7+
8+
from ._utils import is_mapping
9+
from ._models import BaseModel
10+
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
11+
12+
__all__ = ["SyncMyOffsetPage", "AsyncMyOffsetPage"]
13+
14+
_BaseModelT = TypeVar("_BaseModelT", bound=BaseModel)
15+
16+
_T = TypeVar("_T")
17+
18+
19+
class SyncMyOffsetPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
20+
items: List[_T]
21+
22+
@override
23+
def _get_page_items(self) -> List[_T]:
24+
items = self.items
25+
if not items:
26+
return []
27+
return items
28+
29+
@override
30+
def next_page_info(self) -> Optional[PageInfo]:
31+
offset = self._options.params.get("offset") or 0
32+
if not isinstance(offset, int):
33+
raise ValueError(f'Expected "offset" param to be an integer but got {offset}')
34+
35+
length = len(self._get_page_items())
36+
current_count = offset + length
37+
38+
return PageInfo(params={"offset": current_count})
39+
40+
@classmethod
41+
def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
42+
return cls.construct(
43+
None,
44+
**{
45+
**(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
46+
},
47+
)
48+
49+
50+
class AsyncMyOffsetPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
51+
items: List[_T]
52+
53+
@override
54+
def _get_page_items(self) -> List[_T]:
55+
items = self.items
56+
if not items:
57+
return []
58+
return items
59+
60+
@override
61+
def next_page_info(self) -> Optional[PageInfo]:
62+
offset = self._options.params.get("offset") or 0
63+
if not isinstance(offset, int):
64+
raise ValueError(f'Expected "offset" param to be an integer but got {offset}')
65+
66+
length = len(self._get_page_items())
67+
current_count = offset + length
68+
69+
return PageInfo(params={"offset": current_count})
70+
71+
@classmethod
72+
def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
73+
return cls.construct(
74+
None,
75+
**{
76+
**(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
77+
},
78+
)

tests/test_client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,14 @@ def test_base_url_env(self) -> None:
515515
client = Codex(_strict_response_validation=True)
516516
assert client.base_url == "http://localhost:5000/from/env/"
517517

518+
# explicit environment arg requires explicitness
519+
with update_env(CODEX_BASE_URL="http://localhost:5000/from/env"):
520+
with pytest.raises(ValueError, match=r"you must pass base_url=None"):
521+
Codex(_strict_response_validation=True, environment="production")
522+
523+
client = Codex(base_url=None, _strict_response_validation=True, environment="production")
524+
assert str(client.base_url).startswith("https://api-alpha-o3gxj3oajfu.cleanlab.ai")
525+
518526
@pytest.mark.parametrize(
519527
"client",
520528
[
@@ -1248,6 +1256,14 @@ def test_base_url_env(self) -> None:
12481256
client = AsyncCodex(_strict_response_validation=True)
12491257
assert client.base_url == "http://localhost:5000/from/env/"
12501258

1259+
# explicit environment arg requires explicitness
1260+
with update_env(CODEX_BASE_URL="http://localhost:5000/from/env"):
1261+
with pytest.raises(ValueError, match=r"you must pass base_url=None"):
1262+
AsyncCodex(_strict_response_validation=True, environment="production")
1263+
1264+
client = AsyncCodex(base_url=None, _strict_response_validation=True, environment="production")
1265+
assert str(client.base_url).startswith("https://api-alpha-o3gxj3oajfu.cleanlab.ai")
1266+
12511267
@pytest.mark.parametrize(
12521268
"client",
12531269
[

0 commit comments

Comments
 (0)