oauth-codex 2.3.0__tar.gz → 2.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/PKG-INFO +7 -1
  2. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/README.md +6 -0
  3. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/pyproject.toml +1 -1
  4. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/_client.py +230 -0
  5. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/_exceptions.py +93 -0
  6. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/core_types.py +16 -0
  7. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex.egg-info/PKG-INFO +7 -1
  8. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex.egg-info/SOURCES.txt +2 -0
  9. oauth_codex-2.3.1/tests/test_client_authentication.py +46 -0
  10. oauth_codex-2.3.1/tests/test_public_api_docstrings.py +48 -0
  11. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/setup.cfg +0 -0
  12. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/__init__.py +0 -0
  13. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/_base_client.py +0 -0
  14. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/_engine.py +0 -0
  15. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/_models.py +0 -0
  16. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/_module_client.py +0 -0
  17. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/_resource.py +0 -0
  18. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/_types.py +0 -0
  19. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/_version.py +0 -0
  20. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/auth/__init__.py +0 -0
  21. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/auth/config.py +0 -0
  22. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/auth/pkce.py +0 -0
  23. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/auth/store.py +0 -0
  24. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/auth/token_manager.py +0 -0
  25. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/compat_store.py +0 -0
  26. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/errors.py +0 -0
  27. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/py.typed +0 -0
  28. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/__init__.py +0 -0
  29. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/_wrappers.py +0 -0
  30. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/files.py +0 -0
  31. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/models.py +0 -0
  32. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/responses/__init__.py +0 -0
  33. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/responses/_helpers.py +0 -0
  34. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/responses/input_tokens.py +0 -0
  35. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/responses/responses.py +0 -0
  36. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/vector_stores/__init__.py +0 -0
  37. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/vector_stores/file_batches.py +0 -0
  38. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/vector_stores/files.py +0 -0
  39. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/vector_stores/vector_stores.py +0 -0
  40. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/store.py +0 -0
  41. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/tooling.py +0 -0
  42. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/__init__.py +0 -0
  43. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/file_deleted.py +0 -0
  44. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/file_object.py +0 -0
  45. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/responses/__init__.py +0 -0
  46. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/responses/input_token_count_response.py +0 -0
  47. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/responses/response.py +0 -0
  48. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/responses/response_stream_event.py +0 -0
  49. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/shared/__init__.py +0 -0
  50. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/shared/model_capabilities.py +0 -0
  51. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/shared/usage.py +0 -0
  52. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/__init__.py +0 -0
  53. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/vector_store.py +0 -0
  54. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/vector_store_deleted.py +0 -0
  55. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/vector_store_file.py +0 -0
  56. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/vector_store_file_batch.py +0 -0
  57. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/vector_store_search_response.py +0 -0
  58. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex/version.py +0 -0
  59. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex.egg-info/dependency_links.txt +0 -0
  60. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex.egg-info/requires.txt +0 -0
  61. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/src/oauth_codex.egg-info/top_level.txt +0 -0
  62. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/tests/test_engine_stream_and_continuity.py +0 -0
  63. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/tests/test_generate_async.py +0 -0
  64. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/tests/test_generate_sync.py +0 -0
  65. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/tests/test_public_surface.py +0 -0
  66. {oauth_codex-2.3.0 → oauth_codex-2.3.1}/tests/test_tooling_strict_schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oauth-codex
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Codex OAuth-based Python SDK with a single Client and generate-first API
5
5
  Author: Codex
6
6
  Requires-Python: >=3.11
@@ -41,6 +41,12 @@ text = client.generate([{"role": "user", "content": "hello"}])
41
41
  print(text)
42
42
  ```
43
43
 
44
+ Authenticate immediately during client construction:
45
+
46
+ ```python
47
+ client = Client(authenticate_on_init=True)
48
+ ```
49
+
44
50
  ## Image Input
45
51
 
46
52
  ```python
@@ -27,6 +27,12 @@ text = client.generate([{"role": "user", "content": "hello"}])
27
27
  print(text)
28
28
  ```
29
29
 
30
+ Authenticate immediately during client construction:
31
+
32
+ ```python
33
+ client = Client(authenticate_on_init=True)
34
+ ```
35
+
30
36
  ## Image Input
31
37
 
32
38
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "oauth-codex"
7
- version = "2.3.0"
7
+ version = "2.3.1"
8
8
  description = "Codex OAuth-based Python SDK with a single Client and generate-first API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -26,6 +26,29 @@ StructuredOutputSchema = type[BaseModel] | dict[str, Any]
26
26
 
27
27
 
28
28
  class OAuthCodexClient(SyncAPIClient):
29
+ """Public single-entry client for oauth-codex.
30
+
31
+ This client wraps the OAuth-backed responses engine and exposes a
32
+ generate-first workflow:
33
+
34
+ - `generate` and `agenerate` return final text, or a validated JSON object
35
+ when `output_schema` is provided.
36
+ - `stream` and `astream` yield text deltas while still supporting
37
+ automatic tool-call continuation rounds.
38
+
39
+ Public attributes:
40
+ default_model: Model used when `model` is omitted. Defaults to
41
+ `"gpt-5.3-codex"`.
42
+ max_tool_rounds: Maximum number of automatic tool continuation rounds
43
+ before raising `RuntimeError`.
44
+
45
+ Example:
46
+ from oauth_codex import Client
47
+
48
+ client = Client(authenticate_on_init=True)
49
+ text = client.generate([{"role": "user", "content": "hello"}])
50
+ """
51
+
29
52
  def __init__(
30
53
  self,
31
54
  *,
@@ -40,7 +63,28 @@ class OAuthCodexClient(SyncAPIClient):
40
63
  on_request_end: Any | None = None,
41
64
  on_auth_refresh: Any | None = None,
42
65
  on_error: Any | None = None,
66
+ authenticate_on_init: bool = False,
43
67
  ) -> None:
68
+ """Create a `Client` instance.
69
+
70
+ Args:
71
+ oauth_config: Optional OAuth endpoint/client configuration.
72
+ token_store: Token persistence backend. If omitted, a fallback
73
+ keyring/file store is used.
74
+ base_url: Base URL for Codex backend requests.
75
+ chatgpt_base_url: Legacy alias for `base_url`. `base_url` takes
76
+ precedence when both are provided.
77
+ timeout: Request timeout in seconds.
78
+ max_retries: Number of retry attempts for retryable failures.
79
+ compat_storage_dir: Optional local compatibility storage path.
80
+ on_request_start: Optional callback invoked when a request starts.
81
+ on_request_end: Optional callback invoked when a request ends.
82
+ on_auth_refresh: Optional callback invoked after token refresh.
83
+ on_error: Optional callback invoked when request/auth errors occur.
84
+ authenticate_on_init: When `True`, perform authentication during
85
+ client construction. If no stored token exists, interactive
86
+ login is started immediately.
87
+ """
44
88
  super().__init__()
45
89
 
46
90
  resolved_base_url = (
@@ -63,23 +107,76 @@ class OAuthCodexClient(SyncAPIClient):
63
107
  )
64
108
  self.default_model = DEFAULT_MODEL
65
109
  self.max_tool_rounds = DEFAULT_MAX_TOOL_ROUNDS
110
+ if authenticate_on_init:
111
+ self.authenticate()
66
112
 
67
113
  def is_authenticated(self) -> bool:
114
+ """Return whether usable OAuth tokens are currently available.
115
+
116
+ Returns:
117
+ `True` when a token is available, otherwise `False`.
118
+ """
68
119
  return self._engine.is_authenticated()
69
120
 
70
121
  def is_expired(self, *, leeway_seconds: int = 60) -> bool:
122
+ """Return whether the current token is expired or near expiry.
123
+
124
+ Args:
125
+ leeway_seconds: Safety margin in seconds added before expiry check.
126
+
127
+ Returns:
128
+ `True` if the token is expired within the given leeway.
129
+ """
71
130
  return self._engine.is_expired(leeway_seconds=leeway_seconds)
72
131
 
73
132
  def refresh_if_needed(self, *, force: bool = False) -> bool:
133
+ """Refresh tokens when needed.
134
+
135
+ Args:
136
+ force: Refresh even when token is not currently expired.
137
+
138
+ Returns:
139
+ `True` if refresh happened and succeeded, otherwise `False`.
140
+ """
74
141
  return self._engine.refresh_if_needed(force=force)
75
142
 
76
143
  async def arefresh_if_needed(self, *, force: bool = False) -> bool:
144
+ """Async version of `refresh_if_needed`.
145
+
146
+ Args:
147
+ force: Refresh even when token is not currently expired.
148
+
149
+ Returns:
150
+ `True` if refresh happened and succeeded, otherwise `False`.
151
+ """
77
152
  return await self._engine.arefresh_if_needed(force=force)
78
153
 
154
+ def authenticate(self) -> None:
155
+ """Ensure usable OAuth credentials are available now.
156
+
157
+ This loads stored credentials, starts interactive login when missing,
158
+ and refreshes tokens when expired.
159
+ """
160
+ self._engine._ensure_authenticated_sync()
161
+
79
162
  def login(self) -> None:
163
+ """Run interactive OAuth login flow in the current thread.
164
+
165
+ Raises:
166
+ OAuthCallbackParseError: If callback URL parsing fails.
167
+ OAuthStateMismatchError: If OAuth state verification fails.
168
+ TokenExchangeError: If code-to-token exchange fails.
169
+ """
80
170
  self._engine.login()
81
171
 
82
172
  async def alogin(self) -> None:
173
+ """Run interactive OAuth login flow without blocking the event loop.
174
+
175
+ Raises:
176
+ OAuthCallbackParseError: If callback URL parsing fails.
177
+ OAuthStateMismatchError: If OAuth state verification fails.
178
+ TokenExchangeError: If code-to-token exchange fails.
179
+ """
83
180
  await asyncio.to_thread(self._engine.login)
84
181
 
85
182
  def generate(
@@ -95,6 +192,53 @@ class OAuthCodexClient(SyncAPIClient):
95
192
  output_schema: StructuredOutputSchema | None = None,
96
193
  strict_output: bool | None = None,
97
194
  ) -> str | dict[str, Any]:
195
+ """Generate a final response with automatic tool execution.
196
+
197
+ Args:
198
+ messages: Non-empty list of response input messages.
199
+ tools: Optional list of Python callables used for automatic
200
+ function-calling rounds.
201
+ model: Optional model override. Uses `default_model` when omitted.
202
+ reasoning_effort: Reasoning intensity (`"low"`, `"medium"`,
203
+ `"high"`).
204
+ temperature: Sampling temperature.
205
+ top_p: Nucleus sampling probability.
206
+ max_output_tokens: Maximum generated output tokens.
207
+ output_schema: Optional structured-output schema. Accepts a
208
+ Pydantic model type or JSON schema dict.
209
+ strict_output: Strict schema mode. When `output_schema` is set and
210
+ `strict_output` is omitted, strict mode is enabled by default.
211
+
212
+ Returns:
213
+ Final text output, or a validated JSON object when `output_schema`
214
+ is provided.
215
+
216
+ Raises:
217
+ ValueError: If `messages` is missing/empty, or structured output is
218
+ not valid JSON.
219
+ TypeError: If `messages` is not a list, `tools` are invalid, or
220
+ structured output is not a JSON object.
221
+ RuntimeError: If automatic tool execution exceeds
222
+ `max_tool_rounds`.
223
+ pydantic.ValidationError: If a Pydantic `output_schema` fails
224
+ strict validation.
225
+
226
+ Examples:
227
+ Basic text generation:
228
+ text = client.generate([{"role": "user", "content": "hello"}])
229
+
230
+ Structured output:
231
+ from pydantic import BaseModel
232
+
233
+ class Summary(BaseModel):
234
+ title: str
235
+ score: int
236
+
237
+ data = client.generate(
238
+ [{"role": "user", "content": "Return JSON"}],
239
+ output_schema=Summary,
240
+ )
241
+ """
98
242
  messages = self._normalize_initial_messages(messages)
99
243
  normalized_tools, tools_by_name = self._normalize_tools(tools)
100
244
  response_format, effective_strict_output = self._resolve_structured_output_options(
@@ -155,6 +299,40 @@ class OAuthCodexClient(SyncAPIClient):
155
299
  output_schema: StructuredOutputSchema | None = None,
156
300
  strict_output: bool | None = None,
157
301
  ) -> str | dict[str, Any]:
302
+ """Async response generation with automatic tool execution.
303
+
304
+ Args:
305
+ messages: Non-empty list of response input messages.
306
+ tools: Optional list of Python callables used for automatic
307
+ function-calling rounds.
308
+ model: Optional model override. Uses `default_model` when omitted.
309
+ reasoning_effort: Reasoning intensity (`"low"`, `"medium"`,
310
+ `"high"`).
311
+ temperature: Sampling temperature.
312
+ top_p: Nucleus sampling probability.
313
+ max_output_tokens: Maximum generated output tokens.
314
+ output_schema: Optional structured-output schema. Accepts a
315
+ Pydantic model type or JSON schema dict.
316
+ strict_output: Strict schema mode. When `output_schema` is set and
317
+ `strict_output` is omitted, strict mode is enabled by default.
318
+
319
+ Returns:
320
+ Final text output, or a validated JSON object when `output_schema`
321
+ is provided.
322
+
323
+ Raises:
324
+ ValueError: If `messages` is missing/empty, or structured output is
325
+ not valid JSON.
326
+ TypeError: If `messages` is not a list, `tools` are invalid, or
327
+ structured output is not a JSON object.
328
+ RuntimeError: If automatic tool execution exceeds
329
+ `max_tool_rounds`.
330
+ pydantic.ValidationError: If a Pydantic `output_schema` fails
331
+ strict validation.
332
+
333
+ Examples:
334
+ text = await client.agenerate([{"role": "user", "content": "hello"}])
335
+ """
158
336
  messages = self._normalize_initial_messages(messages)
159
337
  normalized_tools, tools_by_name = self._normalize_tools(tools)
160
338
  response_format, effective_strict_output = self._resolve_structured_output_options(
@@ -215,6 +393,32 @@ class OAuthCodexClient(SyncAPIClient):
215
393
  output_schema: StructuredOutputSchema | None = None,
216
394
  strict_output: bool | None = None,
217
395
  ) -> Iterator[str]:
396
+ """Stream text deltas with automatic tool execution rounds.
397
+
398
+ Args:
399
+ messages: Non-empty list of response input messages.
400
+ tools: Optional list of Python callables used for automatic
401
+ function-calling rounds.
402
+ model: Optional model override. Uses `default_model` when omitted.
403
+ reasoning_effort: Reasoning intensity (`"low"`, `"medium"`,
404
+ `"high"`).
405
+ temperature: Sampling temperature.
406
+ top_p: Nucleus sampling probability.
407
+ max_output_tokens: Maximum generated output tokens.
408
+ output_schema: Optional structured-output schema forwarded to the
409
+ backend. Stream output is still text deltas only.
410
+ strict_output: Strict schema mode. When `output_schema` is set and
411
+ `strict_output` is omitted, strict mode is enabled by default.
412
+
413
+ Yields:
414
+ Text delta chunks as they arrive.
415
+
416
+ Raises:
417
+ ValueError: If `messages` is missing/empty.
418
+ TypeError: If `messages` is not a list or `tools` are invalid.
419
+ RuntimeError: If automatic tool execution exceeds
420
+ `max_tool_rounds`.
421
+ """
218
422
  messages = self._normalize_initial_messages(messages)
219
423
  normalized_tools, tools_by_name = self._normalize_tools(tools)
220
424
  response_format, effective_strict_output = self._resolve_structured_output_options(
@@ -275,6 +479,32 @@ class OAuthCodexClient(SyncAPIClient):
275
479
  output_schema: StructuredOutputSchema | None = None,
276
480
  strict_output: bool | None = None,
277
481
  ) -> AsyncIterator[str]:
482
+ """Async stream of text deltas with automatic tool execution rounds.
483
+
484
+ Args:
485
+ messages: Non-empty list of response input messages.
486
+ tools: Optional list of Python callables used for automatic
487
+ function-calling rounds.
488
+ model: Optional model override. Uses `default_model` when omitted.
489
+ reasoning_effort: Reasoning intensity (`"low"`, `"medium"`,
490
+ `"high"`).
491
+ temperature: Sampling temperature.
492
+ top_p: Nucleus sampling probability.
493
+ max_output_tokens: Maximum generated output tokens.
494
+ output_schema: Optional structured-output schema forwarded to the
495
+ backend. Stream output is still text deltas only.
496
+ strict_output: Strict schema mode. When `output_schema` is set and
497
+ `strict_output` is omitted, strict mode is enabled by default.
498
+
499
+ Yields:
500
+ Text delta chunks as they arrive.
501
+
502
+ Raises:
503
+ ValueError: If `messages` is missing/empty.
504
+ TypeError: If `messages` is not a list or `tools` are invalid.
505
+ RuntimeError: If automatic tool execution exceeds
506
+ `max_tool_rounds`.
507
+ """
278
508
  messages = self._normalize_initial_messages(messages)
279
509
  normalized_tools, tools_by_name = self._normalize_tools(tools)
280
510
  response_format, effective_strict_output = self._resolve_structured_output_options(
@@ -6,14 +6,29 @@ import httpx
6
6
 
7
7
 
8
8
  class OAuthCodexError(Exception):
9
+ """Base class for all public oauth-codex exceptions."""
10
+
9
11
  pass
10
12
 
11
13
 
12
14
  class OpenAIError(OAuthCodexError):
15
+ """Compatibility base error alias for OpenAI-style exception handling."""
16
+
13
17
  pass
14
18
 
15
19
 
16
20
  class APIError(OpenAIError):
21
+ """Base error for request/response failures returned by the API layer.
22
+
23
+ Attributes:
24
+ message: Human-readable error message.
25
+ request: Originating `httpx.Request` when available.
26
+ body: Raw parsed error body when available.
27
+ code: Provider error code extracted from `body` when present.
28
+ param: Provider parameter field extracted from `body` when present.
29
+ type: Provider error type extracted from `body` when present.
30
+ """
31
+
17
32
  def __init__(self, message: str, request: httpx.Request | None = None, *, body: object | None = None) -> None:
18
33
  super().__init__(message)
19
34
  self.message = message
@@ -32,6 +47,14 @@ class APIError(OpenAIError):
32
47
 
33
48
 
34
49
  class APIStatusError(APIError):
50
+ """HTTP status error with access to response metadata.
51
+
52
+ Attributes:
53
+ response: Full `httpx.Response`.
54
+ status_code: HTTP status code from the response.
55
+ request_id: Request identifier from `x-request-id` header when present.
56
+ """
57
+
35
58
  def __init__(self, message: str, *, response: httpx.Response, body: object | None = None) -> None:
36
59
  super().__init__(message, response.request, body=body)
37
60
  self.response = response
@@ -40,48 +63,70 @@ class APIStatusError(APIError):
40
63
 
41
64
 
42
65
  class APIConnectionError(APIError):
66
+ """Network-level connection failure while talking to the API."""
67
+
43
68
  def __init__(self, *, message: str = "Connection error.", request: httpx.Request | None = None) -> None:
44
69
  super().__init__(message, request, body=None)
45
70
 
46
71
 
47
72
  class APITimeoutError(APIConnectionError):
73
+ """Request timeout while waiting for API response."""
74
+
48
75
  def __init__(self, request: httpx.Request | None = None) -> None:
49
76
  super().__init__(message="Request timed out.", request=request)
50
77
 
51
78
 
52
79
  class BadRequestError(APIStatusError):
80
+ """HTTP 400 response from the API."""
81
+
53
82
  pass
54
83
 
55
84
 
56
85
  class AuthenticationError(APIStatusError):
86
+ """HTTP 401 response from the API."""
87
+
57
88
  pass
58
89
 
59
90
 
60
91
  class PermissionDeniedError(APIStatusError):
92
+ """HTTP 403 response from the API."""
93
+
61
94
  pass
62
95
 
63
96
 
64
97
  class NotFoundError(APIStatusError):
98
+ """HTTP 404 response from the API."""
99
+
65
100
  pass
66
101
 
67
102
 
68
103
  class ConflictError(APIStatusError):
104
+ """HTTP 409 response from the API."""
105
+
69
106
  pass
70
107
 
71
108
 
72
109
  class UnprocessableEntityError(APIStatusError):
110
+ """HTTP 422 response from the API."""
111
+
73
112
  pass
74
113
 
75
114
 
76
115
  class RateLimitError(APIStatusError):
116
+ """HTTP 429 response from the API."""
117
+
77
118
  pass
78
119
 
79
120
 
80
121
  class InternalServerError(APIStatusError):
122
+ """HTTP 5xx server error returned by the API."""
123
+
81
124
  pass
82
125
 
83
126
 
84
127
  class APIResponseValidationError(APIError):
128
+ """API response payload failed SDK-side schema validation."""
129
+
85
130
  def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None:
86
131
  super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body)
87
132
  self.response = response
@@ -89,56 +134,95 @@ class APIResponseValidationError(APIError):
89
134
 
90
135
 
91
136
  class OAuthCallbackParseError(OAuthCodexError):
137
+ """OAuth browser callback could not be parsed."""
138
+
92
139
  pass
93
140
 
94
141
 
95
142
  class OAuthStateMismatchError(OAuthCodexError):
143
+ """OAuth callback state does not match the initiated login request."""
144
+
96
145
  pass
97
146
 
98
147
 
99
148
  class TokenExchangeError(OAuthCodexError):
149
+ """OAuth authorization code exchange for tokens failed."""
150
+
100
151
  pass
101
152
 
102
153
 
103
154
  class TokenRefreshError(OAuthCodexError):
155
+ """Refreshing an existing OAuth token failed."""
156
+
104
157
  pass
105
158
 
106
159
 
107
160
  class AuthRequiredError(OAuthCodexError):
161
+ """Authenticated call was attempted without valid credentials."""
162
+
108
163
  pass
109
164
 
110
165
 
111
166
  class ParameterValidationError(OAuthCodexError):
167
+ """Input parameters failed SDK validation before sending request."""
168
+
112
169
  pass
113
170
 
114
171
 
115
172
  class ModelValidationError(OAuthCodexError):
173
+ """Model capability or model name validation failed."""
174
+
116
175
  pass
117
176
 
118
177
 
119
178
  class ContinuityError(OAuthCodexError):
179
+ """Response continuity state is invalid for continuation requests."""
180
+
120
181
  pass
121
182
 
122
183
 
123
184
  class TokenStoreError(OAuthCodexError):
185
+ """Base class for token persistence layer failures.
186
+
187
+ Attributes:
188
+ cause: Original exception raised by the storage backend, when present.
189
+ """
190
+
124
191
  def __init__(self, message: str, *, cause: Exception | None = None) -> None:
125
192
  super().__init__(message)
126
193
  self.cause = cause
127
194
 
128
195
 
129
196
  class TokenStoreReadError(TokenStoreError):
197
+ """Token load/read operation failed."""
198
+
130
199
  pass
131
200
 
132
201
 
133
202
  class TokenStoreWriteError(TokenStoreError):
203
+ """Token save/write operation failed."""
204
+
134
205
  pass
135
206
 
136
207
 
137
208
  class TokenStoreDeleteError(TokenStoreError):
209
+ """Token delete operation failed."""
210
+
138
211
  pass
139
212
 
140
213
 
141
214
  class SDKRequestError(OAuthCodexError):
215
+ """Normalized SDK error for provider and compatibility request failures.
216
+
217
+ Attributes:
218
+ status_code: HTTP-like status code when available.
219
+ provider_code: Provider-specific machine-readable error code.
220
+ user_message: Human-readable message intended for callers.
221
+ retryable: Whether the request may succeed on retry.
222
+ request_id: Provider request identifier when available.
223
+ raw_error: Original provider payload or underlying exception data.
224
+ """
225
+
142
226
  def __init__(
143
227
  self,
144
228
  *,
@@ -159,6 +243,8 @@ class SDKRequestError(OAuthCodexError):
159
243
 
160
244
 
161
245
  class NotSupportedError(SDKRequestError):
246
+ """Requested operation is not supported by the active backend/profile."""
247
+
162
248
  def __init__(self, message: str, *, code: str = "not_supported") -> None:
163
249
  super().__init__(
164
250
  status_code=400,
@@ -170,6 +256,13 @@ class NotSupportedError(SDKRequestError):
170
256
 
171
257
 
172
258
  class ToolCallRequiredError(OAuthCodexError):
259
+ """Model response requires tool execution before completion.
260
+
261
+ Attributes:
262
+ message: Human-readable explanation for the tool requirement.
263
+ tool_calls: Tool call payloads that must be executed.
264
+ """
265
+
173
266
  def __init__(self, message: str, tool_calls: list[dict[str, Any]] | None = None) -> None:
174
267
  super().__init__(message)
175
268
  self.message = message
@@ -1,10 +1,26 @@
1
+ """Core public data types used by oauth-codex.
2
+
3
+ `Message` represents a single response input message dictionary, and
4
+ `listMessage` is the list container passed to `Client.generate`,
5
+ `Client.stream`, `Client.agenerate`, and `Client.astream`.
6
+
7
+ Recommended minimal message shape:
8
+
9
+ [{"role": "user", "content": "hello"}]
10
+
11
+ `content` may also be a list of input items such as `input_text` and
12
+ `input_image`.
13
+ """
14
+
1
15
  from __future__ import annotations
2
16
 
3
17
  from dataclasses import dataclass, field
4
18
  from pathlib import Path
5
19
  from typing import Any, Callable, Literal, Protocol, TypeAlias
6
20
 
21
+ #: A single response input message item (for example role/content dict).
7
22
  Message: TypeAlias = dict[str, Any]
23
+ #: List of response input messages accepted by `Client` generation methods.
8
24
  listMessage: TypeAlias = list[Message]
9
25
  ValidationMode: TypeAlias = Literal["warn", "error", "ignore"]
10
26
  StoreBehavior: TypeAlias = Literal["auto_disable", "error", "passthrough"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oauth-codex
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Codex OAuth-based Python SDK with a single Client and generate-first API
5
5
  Author: Codex
6
6
  Requires-Python: >=3.11
@@ -41,6 +41,12 @@ text = client.generate([{"role": "user", "content": "hello"}])
41
41
  print(text)
42
42
  ```
43
43
 
44
+ Authenticate immediately during client construction:
45
+
46
+ ```python
47
+ client = Client(authenticate_on_init=True)
48
+ ```
49
+
44
50
  ## Image Input
45
51
 
46
52
  ```python
@@ -55,8 +55,10 @@ src/oauth_codex/types/vector_stores/vector_store_deleted.py
55
55
  src/oauth_codex/types/vector_stores/vector_store_file.py
56
56
  src/oauth_codex/types/vector_stores/vector_store_file_batch.py
57
57
  src/oauth_codex/types/vector_stores/vector_store_search_response.py
58
+ tests/test_client_authentication.py
58
59
  tests/test_engine_stream_and_continuity.py
59
60
  tests/test_generate_async.py
60
61
  tests/test_generate_sync.py
62
+ tests/test_public_api_docstrings.py
61
63
  tests/test_public_surface.py
62
64
  tests/test_tooling_strict_schema.py
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from conftest import InMemoryTokenStore
6
+ from oauth_codex import Client
7
+ from oauth_codex._client import _EngineClient
8
+ from oauth_codex.core_types import OAuthTokens
9
+
10
+
11
+ def _tokens() -> OAuthTokens:
12
+ return OAuthTokens(access_token="a", refresh_token="r", expires_at=9_999_999_999)
13
+
14
+
15
+ def test_client_does_not_authenticate_on_init_by_default(
16
+ monkeypatch: pytest.MonkeyPatch,
17
+ ) -> None:
18
+ ensure_calls = 0
19
+
20
+ def fake_ensure(self: _EngineClient) -> OAuthTokens:
21
+ nonlocal ensure_calls
22
+ ensure_calls += 1
23
+ return _tokens()
24
+
25
+ monkeypatch.setattr(_EngineClient, "_ensure_authenticated_sync", fake_ensure)
26
+
27
+ Client(token_store=InMemoryTokenStore(_tokens()))
28
+
29
+ assert ensure_calls == 0
30
+
31
+
32
+ def test_client_authenticates_on_init_when_enabled(
33
+ monkeypatch: pytest.MonkeyPatch,
34
+ ) -> None:
35
+ ensure_calls = 0
36
+
37
+ def fake_ensure(self: _EngineClient) -> OAuthTokens:
38
+ nonlocal ensure_calls
39
+ ensure_calls += 1
40
+ return _tokens()
41
+
42
+ monkeypatch.setattr(_EngineClient, "_ensure_authenticated_sync", fake_ensure)
43
+
44
+ Client(token_store=InMemoryTokenStore(), authenticate_on_init=True)
45
+
46
+ assert ensure_calls == 1
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+
5
+ import oauth_codex
6
+ import oauth_codex.core_types as core_types
7
+
8
+
9
+ def test_client_class_docstring_exists() -> None:
10
+ doc = inspect.getdoc(oauth_codex.Client)
11
+ assert doc is not None
12
+ assert "default_model" in doc
13
+ assert "max_tool_rounds" in doc
14
+
15
+
16
+ def test_generate_docstring_includes_key_sections() -> None:
17
+ doc = inspect.getdoc(oauth_codex.Client.generate)
18
+ assert doc is not None
19
+ for keyword in ("messages", "tools", "output_schema", "Returns", "Raises"):
20
+ assert keyword in doc
21
+
22
+
23
+ def test_agenerate_docstring_includes_key_sections() -> None:
24
+ doc = inspect.getdoc(oauth_codex.Client.agenerate)
25
+ assert doc is not None
26
+ for keyword in ("messages", "tools", "output_schema", "Returns", "Raises"):
27
+ assert keyword in doc
28
+
29
+
30
+ def test_stream_docstring_includes_streaming_context() -> None:
31
+ doc = inspect.getdoc(oauth_codex.Client.stream)
32
+ assert doc is not None
33
+ for keyword in ("messages", "tools", "output_schema", "Yields", "Raises"):
34
+ assert keyword in doc
35
+
36
+
37
+ def test_root_exported_exceptions_have_docstrings() -> None:
38
+ for name in oauth_codex.__all__:
39
+ exported = getattr(oauth_codex, name)
40
+ if inspect.isclass(exported) and issubclass(exported, Exception):
41
+ assert inspect.getdoc(exported), f"missing docstring for {name}"
42
+
43
+
44
+ def test_core_types_module_doc_mentions_list_message() -> None:
45
+ doc = inspect.getdoc(core_types)
46
+ assert doc is not None
47
+ assert "Message" in doc
48
+ assert "listMessage" in doc
File without changes