codex-python-sdk 0.1.0__py3-none-any.whl
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.
- codex_python_sdk/__init__.py +57 -0
- codex_python_sdk/_shared.py +99 -0
- codex_python_sdk/async_client.py +1313 -0
- codex_python_sdk/errors.py +18 -0
- codex_python_sdk/examples/__init__.py +2 -0
- codex_python_sdk/examples/demo_smoke.py +304 -0
- codex_python_sdk/factory.py +25 -0
- codex_python_sdk/policy.py +636 -0
- codex_python_sdk/renderer.py +607 -0
- codex_python_sdk/sync_client.py +333 -0
- codex_python_sdk/types.py +48 -0
- codex_python_sdk-0.1.0.dist-info/METADATA +274 -0
- codex_python_sdk-0.1.0.dist-info/RECORD +17 -0
- codex_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- codex_python_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- codex_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- codex_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CodexAgenticError(RuntimeError):
|
|
5
|
+
"""Base error for all client failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AppServerConnectionError(CodexAgenticError):
|
|
9
|
+
"""Raised when app-server transport/session setup or request execution fails."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SessionNotFoundError(CodexAgenticError):
|
|
13
|
+
"""Raised when the requested session id is unknown."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotAuthenticatedError(CodexAgenticError):
|
|
17
|
+
"""Raised when Codex CLI authentication is unavailable or invalid."""
|
|
18
|
+
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import time
|
|
5
|
+
import traceback
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from codex_python_sdk import create_client, render_exec_style_events
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class DemoResult:
|
|
14
|
+
name: str
|
|
15
|
+
ok: bool
|
|
16
|
+
detail: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _truncate(value: Any, limit: int = 120) -> str:
|
|
20
|
+
text = str(value)
|
|
21
|
+
if len(text) <= limit:
|
|
22
|
+
return text
|
|
23
|
+
return text[: limit - 3] + "..."
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _find_first_string(value: Any, keys: set[str]) -> str | None:
|
|
27
|
+
if isinstance(value, dict):
|
|
28
|
+
for key in keys:
|
|
29
|
+
maybe = value.get(key)
|
|
30
|
+
if isinstance(maybe, str) and maybe:
|
|
31
|
+
return maybe
|
|
32
|
+
for nested in value.values():
|
|
33
|
+
found = _find_first_string(nested, keys)
|
|
34
|
+
if found:
|
|
35
|
+
return found
|
|
36
|
+
elif isinstance(value, list):
|
|
37
|
+
for nested in value:
|
|
38
|
+
found = _find_first_string(nested, keys)
|
|
39
|
+
if found:
|
|
40
|
+
return found
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _run_case(
|
|
45
|
+
results: list[DemoResult],
|
|
46
|
+
name: str,
|
|
47
|
+
fn: Callable[[], Any],
|
|
48
|
+
*,
|
|
49
|
+
strict: bool,
|
|
50
|
+
) -> Any:
|
|
51
|
+
try:
|
|
52
|
+
value = fn()
|
|
53
|
+
results.append(DemoResult(name=name, ok=True, detail=_truncate(value)))
|
|
54
|
+
print(f"[PASS] {name}: {_truncate(value)}")
|
|
55
|
+
return value
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
detail = f"{type(exc).__name__}: {exc}"
|
|
58
|
+
results.append(DemoResult(name=name, ok=False, detail=detail))
|
|
59
|
+
print(f"[FAIL] {name}: {detail}")
|
|
60
|
+
if strict:
|
|
61
|
+
raise
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> None:
|
|
66
|
+
parser = argparse.ArgumentParser(description="Codex SDK smoke runner and API demo.")
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--mode",
|
|
69
|
+
choices=("smoke", "demo", "full"),
|
|
70
|
+
default="smoke",
|
|
71
|
+
help="smoke: quick health check; demo: stable API showcase; full: demo + unstable remote cases.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument("--model", default=None, help="Optional model for thread/start.")
|
|
74
|
+
parser.add_argument("--strict", action="store_true", help="Fail fast on first interface failure.")
|
|
75
|
+
parser.add_argument("--show-events", action="store_true", help="Render event stream in exec-style.")
|
|
76
|
+
parser.add_argument("--prompt-create", default="Reply with exactly: READY", help="Prompt for responses_create.")
|
|
77
|
+
parser.add_argument("--prompt-events", default="List three files in current directory.", help="Prompt for responses_events.")
|
|
78
|
+
parser.add_argument("--prompt-stream", default="Reply with exactly: STREAM_OK", help="Prompt for responses_stream_text.")
|
|
79
|
+
args = parser.parse_args()
|
|
80
|
+
|
|
81
|
+
results: list[DemoResult] = []
|
|
82
|
+
last_turn_id: str | None = None
|
|
83
|
+
fork_thread_id: str | None = None
|
|
84
|
+
|
|
85
|
+
with create_client(
|
|
86
|
+
on_command_approval=lambda params: {"decision": "accept"},
|
|
87
|
+
on_file_change_approval=lambda params: {"decision": "accept"},
|
|
88
|
+
on_tool_request_user_input=lambda params: {"answers": {}},
|
|
89
|
+
) as client:
|
|
90
|
+
start_params = {"model": args.model} if args.model else None
|
|
91
|
+
bootstrap_thread_id: str | None = None
|
|
92
|
+
started = _run_case(
|
|
93
|
+
results,
|
|
94
|
+
"thread_start",
|
|
95
|
+
lambda: client.thread_start(params=start_params),
|
|
96
|
+
strict=args.strict,
|
|
97
|
+
)
|
|
98
|
+
if isinstance(started, dict):
|
|
99
|
+
thread = started.get("thread")
|
|
100
|
+
if isinstance(thread, dict) and isinstance(thread.get("id"), str):
|
|
101
|
+
bootstrap_thread_id = thread["id"]
|
|
102
|
+
|
|
103
|
+
def _run_create() -> Any:
|
|
104
|
+
if bootstrap_thread_id is not None:
|
|
105
|
+
return client.responses_create(prompt=args.prompt_create, session_id=bootstrap_thread_id)
|
|
106
|
+
return client.responses_create(prompt=args.prompt_create)
|
|
107
|
+
|
|
108
|
+
created = _run_case(
|
|
109
|
+
results,
|
|
110
|
+
"responses_create",
|
|
111
|
+
_run_create,
|
|
112
|
+
strict=args.strict,
|
|
113
|
+
)
|
|
114
|
+
thread_id: str | None = bootstrap_thread_id
|
|
115
|
+
if created is not None and isinstance(created.session_id, str) and created.session_id:
|
|
116
|
+
thread_id = created.session_id
|
|
117
|
+
if thread_id is None:
|
|
118
|
+
print("[INFO] Skipping follow-up cases because no usable session_id is available.")
|
|
119
|
+
|
|
120
|
+
def _run_stream() -> str:
|
|
121
|
+
assert thread_id is not None
|
|
122
|
+
chunks = list(client.responses_stream_text(prompt=args.prompt_stream, session_id=thread_id))
|
|
123
|
+
return "".join(chunks).strip()
|
|
124
|
+
|
|
125
|
+
if thread_id is not None:
|
|
126
|
+
_run_case(results, "responses_stream_text", _run_stream, strict=args.strict)
|
|
127
|
+
|
|
128
|
+
if args.mode != "smoke" and thread_id is not None:
|
|
129
|
+
def _run_events() -> str:
|
|
130
|
+
nonlocal last_turn_id
|
|
131
|
+
event_types: list[str] = []
|
|
132
|
+
event_count = 0
|
|
133
|
+
events = client.responses_events(prompt=args.prompt_events, session_id=thread_id)
|
|
134
|
+
if args.show_events:
|
|
135
|
+
render_exec_style_events(events, show_reasoning=True, show_system=False, show_tool_output=True, color="auto")
|
|
136
|
+
return "rendered by ExecStyleRenderer"
|
|
137
|
+
for event in events:
|
|
138
|
+
event_count += 1
|
|
139
|
+
if len(event_types) < 8:
|
|
140
|
+
event_types.append(event.type)
|
|
141
|
+
if event.turn_id:
|
|
142
|
+
last_turn_id = event.turn_id
|
|
143
|
+
return f"events={event_count}, sample_types={event_types}"
|
|
144
|
+
|
|
145
|
+
_run_case(results, "responses_events", _run_events, strict=args.strict)
|
|
146
|
+
_run_case(results, "thread_read", lambda: client.thread_read(thread_id, include_turns=True), strict=args.strict)
|
|
147
|
+
_run_case(results, "thread_list", lambda: client.thread_list(limit=10), strict=args.strict)
|
|
148
|
+
_run_case(
|
|
149
|
+
results,
|
|
150
|
+
"thread_name_set",
|
|
151
|
+
lambda: client.thread_name_set(thread_id, "demo-thread"),
|
|
152
|
+
strict=args.strict,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
forked = _run_case(
|
|
156
|
+
results,
|
|
157
|
+
"thread_fork",
|
|
158
|
+
lambda: client.thread_fork(thread_id, params={"approvalPolicy": "on-request"}),
|
|
159
|
+
strict=args.strict,
|
|
160
|
+
)
|
|
161
|
+
fork_id = _find_first_string(forked, {"threadId", "id"}) if forked is not None else None
|
|
162
|
+
if isinstance(fork_id, str):
|
|
163
|
+
fork_thread_id = fork_id
|
|
164
|
+
|
|
165
|
+
_run_case(results, "thread_loaded_list", lambda: client.thread_loaded_list(limit=5), strict=args.strict)
|
|
166
|
+
_run_case(results, "skills_list", lambda: client.skills_list(force_reload=False), strict=args.strict)
|
|
167
|
+
_run_case(results, "app_list", lambda: client.app_list(limit=5), strict=args.strict)
|
|
168
|
+
_run_case(results, "model_list", lambda: client.model_list(limit=5), strict=args.strict)
|
|
169
|
+
_run_case(results, "account_rate_limits_read", lambda: client.account_rate_limits_read(), strict=args.strict)
|
|
170
|
+
_run_case(results, "account_read", lambda: client.account_read(refresh_token=False), strict=args.strict)
|
|
171
|
+
_run_case(results, "thread_archive", lambda: client.thread_archive(thread_id), strict=args.strict)
|
|
172
|
+
_run_case(results, "thread_unarchive", lambda: client.thread_unarchive(thread_id), strict=args.strict)
|
|
173
|
+
_run_case(
|
|
174
|
+
results,
|
|
175
|
+
"skills_config_write",
|
|
176
|
+
lambda: client.skills_config_write("demo-skill-path", False),
|
|
177
|
+
strict=args.strict,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if args.mode == "full":
|
|
181
|
+
if thread_id is None:
|
|
182
|
+
print("[INFO] Skipped full-mode unstable APIs because no usable session_id is available.")
|
|
183
|
+
unstable_thread_id = None
|
|
184
|
+
else:
|
|
185
|
+
unstable_thread_id = fork_thread_id or thread_id
|
|
186
|
+
if unstable_thread_id is not None:
|
|
187
|
+
remote_read = _run_case(results, "skills_remote_read", lambda: client.skills_remote_read(), strict=args.strict)
|
|
188
|
+
review_out = _run_case(
|
|
189
|
+
results,
|
|
190
|
+
"review_start",
|
|
191
|
+
lambda: client.review_start(unstable_thread_id, {"type": "uncommittedChanges"}, delivery="detached"),
|
|
192
|
+
strict=args.strict,
|
|
193
|
+
)
|
|
194
|
+
review_turn_id = None
|
|
195
|
+
review_thread_id = unstable_thread_id
|
|
196
|
+
if isinstance(review_out, dict):
|
|
197
|
+
review_turn = review_out.get("turn")
|
|
198
|
+
if isinstance(review_turn, dict) and isinstance(review_turn.get("id"), str):
|
|
199
|
+
review_turn_id = review_turn["id"]
|
|
200
|
+
maybe_review_thread_id = review_out.get("reviewThreadId")
|
|
201
|
+
if isinstance(maybe_review_thread_id, str) and maybe_review_thread_id:
|
|
202
|
+
review_thread_id = maybe_review_thread_id
|
|
203
|
+
if not review_turn_id:
|
|
204
|
+
review_turn_id = last_turn_id
|
|
205
|
+
|
|
206
|
+
if isinstance(review_turn_id, str):
|
|
207
|
+
def _interrupt() -> dict[str, Any]:
|
|
208
|
+
assert review_turn_id is not None
|
|
209
|
+
return client.turn_interrupt(review_thread_id, review_turn_id)
|
|
210
|
+
|
|
211
|
+
interrupt_out = _run_case(
|
|
212
|
+
results,
|
|
213
|
+
"turn_interrupt",
|
|
214
|
+
_interrupt,
|
|
215
|
+
strict=args.strict,
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
interrupt_out = None
|
|
219
|
+
print("[INFO] Skipped turn_interrupt because review_start did not return a turn id.")
|
|
220
|
+
|
|
221
|
+
if interrupt_out is not None:
|
|
222
|
+
def _wait_review_turn_terminal() -> dict[str, Any]:
|
|
223
|
+
assert review_turn_id is not None
|
|
224
|
+
deadline = time.monotonic() + 30.0
|
|
225
|
+
last_status: str | None = None
|
|
226
|
+
while time.monotonic() < deadline:
|
|
227
|
+
snapshot = client.thread_read(review_thread_id, include_turns=True)
|
|
228
|
+
turns = snapshot.get("turns")
|
|
229
|
+
if isinstance(turns, list):
|
|
230
|
+
for turn in reversed(turns):
|
|
231
|
+
if not isinstance(turn, dict):
|
|
232
|
+
continue
|
|
233
|
+
if turn.get("id") != review_turn_id:
|
|
234
|
+
continue
|
|
235
|
+
maybe_status = turn.get("status")
|
|
236
|
+
if isinstance(maybe_status, str):
|
|
237
|
+
last_status = maybe_status
|
|
238
|
+
if maybe_status != "inProgress":
|
|
239
|
+
return {"terminal": True, "status": maybe_status}
|
|
240
|
+
break
|
|
241
|
+
time.sleep(0.5)
|
|
242
|
+
return {"terminal": False, "status": last_status}
|
|
243
|
+
|
|
244
|
+
wait_out = _run_case(
|
|
245
|
+
results,
|
|
246
|
+
"wait_review_turn_terminal",
|
|
247
|
+
_wait_review_turn_terminal,
|
|
248
|
+
strict=args.strict,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if isinstance(wait_out, dict) and wait_out.get("terminal") is True:
|
|
252
|
+
rollback_thread_id = unstable_thread_id
|
|
253
|
+
_run_case(
|
|
254
|
+
results,
|
|
255
|
+
"thread_rollback",
|
|
256
|
+
lambda: client.thread_rollback(rollback_thread_id, 1),
|
|
257
|
+
strict=args.strict,
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
print("[INFO] Skipped thread_rollback because review turn did not reach terminal status in time.")
|
|
261
|
+
else:
|
|
262
|
+
print("[INFO] Skipped thread_rollback because turn_interrupt did not complete successfully.")
|
|
263
|
+
|
|
264
|
+
_run_case(
|
|
265
|
+
results,
|
|
266
|
+
"thread_compact_start",
|
|
267
|
+
lambda: client.thread_compact_start(unstable_thread_id),
|
|
268
|
+
strict=args.strict,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
hazelnut_id = _find_first_string(remote_read, {"hazelnutId"}) if remote_read is not None else None
|
|
272
|
+
if isinstance(hazelnut_id, str) and hazelnut_id:
|
|
273
|
+
_run_case(
|
|
274
|
+
results,
|
|
275
|
+
"skills_remote_write",
|
|
276
|
+
lambda: client.skills_remote_write(hazelnut_id, False),
|
|
277
|
+
strict=args.strict,
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
print("[INFO] Skipped skills_remote_write because skills_remote_read did not return a hazelnut id.")
|
|
281
|
+
elif args.mode == "demo" and thread_id is not None:
|
|
282
|
+
print("[INFO] Skipped unstable APIs. Use '--mode full' to run remote/interrupt/compact cases.")
|
|
283
|
+
elif args.mode == "smoke":
|
|
284
|
+
print("[INFO] Smoke mode ran core checks only. Use '--mode demo' for broader API coverage.")
|
|
285
|
+
|
|
286
|
+
passed = sum(1 for item in results if item.ok)
|
|
287
|
+
failed = len(results) - passed
|
|
288
|
+
print("\n=== SUMMARY ===")
|
|
289
|
+
print(f"mode={args.mode} total={len(results)} passed={passed} failed={failed}")
|
|
290
|
+
if failed:
|
|
291
|
+
print("failed cases:")
|
|
292
|
+
for item in results:
|
|
293
|
+
if not item.ok:
|
|
294
|
+
print(f"- {item.name}: {item.detail}")
|
|
295
|
+
if args.strict:
|
|
296
|
+
raise SystemExit(1)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
try:
|
|
301
|
+
main()
|
|
302
|
+
except Exception:
|
|
303
|
+
traceback.print_exc()
|
|
304
|
+
raise
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .async_client import AsyncCodexAgenticClient
|
|
6
|
+
from .sync_client import CodexAgenticClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_client(**kwargs: Any) -> CodexAgenticClient:
|
|
10
|
+
"""Create a synchronous client.
|
|
11
|
+
|
|
12
|
+
Keyword args are forwarded to :class:`AsyncCodexAgenticClient`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
return CodexAgenticClient(**kwargs)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_async_client(**kwargs: Any) -> AsyncCodexAgenticClient:
|
|
19
|
+
"""Create an asynchronous client.
|
|
20
|
+
|
|
21
|
+
Keyword args are forwarded to :class:`AsyncCodexAgenticClient`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
return AsyncCodexAgenticClient(**kwargs)
|
|
25
|
+
|