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.
@@ -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,2 @@
1
+ from __future__ import annotations
2
+
@@ -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
+