codex-python 0.1.2__py3-none-any.whl → 0.2.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/__init__.py +12 -8
- codex/api.py +61 -149
- codex/config.py +82 -0
- codex/event.py +16 -0
- codex/native.py +56 -0
- codex/protocol/types.py +413 -289
- {codex_python-0.1.2.dist-info → codex_python-0.2.0.dist-info}/METADATA +69 -30
- codex_python-0.2.0.dist-info/RECORD +11 -0
- codex/protocol/runtime.py +0 -80
- codex_python-0.1.2.dist-info/RECORD +0 -9
- {codex_python-0.1.2.dist-info → codex_python-0.2.0.dist-info}/WHEEL +0 -0
- {codex_python-0.1.2.dist-info → codex_python-0.2.0.dist-info}/licenses/LICENSE +0 -0
codex/__init__.py
CHANGED
@@ -4,27 +4,31 @@ Python interface for the Codex CLI.
|
|
4
4
|
|
5
5
|
Usage:
|
6
6
|
from codex import run_exec
|
7
|
-
|
7
|
+
events = run_exec("explain this codebase to me")
|
8
8
|
"""
|
9
9
|
|
10
10
|
from .api import (
|
11
11
|
CodexClient,
|
12
12
|
CodexError,
|
13
|
-
|
14
|
-
|
15
|
-
find_binary,
|
13
|
+
CodexNativeError,
|
14
|
+
Conversation,
|
16
15
|
run_exec,
|
17
16
|
)
|
17
|
+
from .config import CodexConfig
|
18
|
+
from .event import Event
|
19
|
+
from .protocol.types import EventMsg
|
18
20
|
|
19
21
|
__all__ = [
|
20
22
|
"__version__",
|
21
23
|
"CodexError",
|
22
|
-
"
|
23
|
-
"CodexProcessError",
|
24
|
+
"CodexNativeError",
|
24
25
|
"CodexClient",
|
25
|
-
"
|
26
|
+
"Conversation",
|
26
27
|
"run_exec",
|
28
|
+
"Event",
|
29
|
+
"EventMsg",
|
30
|
+
"CodexConfig",
|
27
31
|
]
|
28
32
|
|
29
33
|
# Managed by Hatch via pyproject.toml [tool.hatch.version]
|
30
|
-
__version__ = "0.
|
34
|
+
__version__ = "0.2.0"
|
codex/api.py
CHANGED
@@ -1,181 +1,93 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
4
|
-
import shutil
|
5
|
-
import subprocess
|
6
|
-
from collections.abc import Iterable, Mapping, Sequence
|
3
|
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
7
4
|
from dataclasses import dataclass
|
8
5
|
|
6
|
+
from .config import CodexConfig
|
7
|
+
from .event import Event
|
8
|
+
from .native import run_exec_collect as native_run_exec_collect
|
9
|
+
from .native import start_exec_stream as native_start_exec_stream
|
10
|
+
|
9
11
|
|
10
12
|
class CodexError(Exception):
|
11
13
|
"""Base exception for codex-python."""
|
12
14
|
|
13
15
|
|
14
|
-
class
|
15
|
-
"""Raised when the
|
16
|
+
class CodexNativeError(CodexError):
|
17
|
+
"""Raised when the native extension is not available or fails."""
|
16
18
|
|
17
|
-
def __init__(self
|
19
|
+
def __init__(self) -> None:
|
18
20
|
super().__init__(
|
19
|
-
|
20
|
-
"
|
21
|
+
"codex_native extension not installed or failed to run. "
|
22
|
+
"Run `make dev-native` or ensure native wheels are installed."
|
21
23
|
)
|
22
|
-
self.executable = executable
|
23
24
|
|
24
25
|
|
25
26
|
@dataclass(slots=True)
|
26
|
-
class
|
27
|
-
"""
|
28
|
-
|
29
|
-
returncode: int
|
30
|
-
cmd: Sequence[str]
|
31
|
-
stdout: str
|
32
|
-
stderr: str
|
33
|
-
|
34
|
-
def __str__(self) -> str: # pragma: no cover - repr is sufficient
|
35
|
-
return (
|
36
|
-
f"Codex process failed with exit code {self.returncode}.\n"
|
37
|
-
f"Command: {' '.join(self.cmd)}\n"
|
38
|
-
f"stderr:\n{self.stderr.strip()}"
|
39
|
-
)
|
40
|
-
|
27
|
+
class Conversation:
|
28
|
+
"""A stateful conversation with Codex, streaming events natively."""
|
41
29
|
|
42
|
-
|
43
|
-
"""Return the absolute path to the Codex CLI binary or raise if not found."""
|
44
|
-
path = shutil.which(executable)
|
45
|
-
if not path:
|
46
|
-
raise CodexNotFoundError(executable)
|
47
|
-
return path
|
30
|
+
_stream: Iterable[dict]
|
48
31
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
model: str | None = None,
|
54
|
-
oss: bool = False,
|
55
|
-
full_auto: bool = False,
|
56
|
-
cd: str | None = None,
|
57
|
-
skip_git_repo_check: bool = False,
|
58
|
-
timeout: float | None = None,
|
59
|
-
env: Mapping[str, str] | None = None,
|
60
|
-
executable: str = "codex",
|
61
|
-
extra_args: Iterable[str] | None = None,
|
62
|
-
json: bool = False,
|
63
|
-
) -> str:
|
64
|
-
"""
|
65
|
-
Run `codex exec` with the given prompt and return stdout as text.
|
66
|
-
|
67
|
-
- Raises CodexNotFoundError if the binary is unavailable.
|
68
|
-
- Raises CodexProcessError on non‑zero exit with captured stdout/stderr.
|
69
|
-
"""
|
70
|
-
bin_path = find_binary(executable)
|
71
|
-
|
72
|
-
cmd: list[str] = [bin_path]
|
73
|
-
|
74
|
-
if cd:
|
75
|
-
cmd.extend(["--cd", cd])
|
76
|
-
if model:
|
77
|
-
cmd.extend(["-m", model])
|
78
|
-
if oss:
|
79
|
-
cmd.append("--oss")
|
80
|
-
if full_auto:
|
81
|
-
cmd.append("--full-auto")
|
82
|
-
if skip_git_repo_check:
|
83
|
-
cmd.append("--skip-git-repo-check")
|
84
|
-
if extra_args:
|
85
|
-
cmd.extend(list(extra_args))
|
86
|
-
|
87
|
-
cmd.append("exec")
|
88
|
-
if json:
|
89
|
-
cmd.append("--json")
|
90
|
-
cmd.append(prompt)
|
91
|
-
|
92
|
-
completed = subprocess.run(
|
93
|
-
cmd,
|
94
|
-
capture_output=True,
|
95
|
-
text=True,
|
96
|
-
timeout=timeout,
|
97
|
-
env={**os.environ, **(dict(env) if env else {})},
|
98
|
-
check=False,
|
99
|
-
)
|
100
|
-
|
101
|
-
stdout = completed.stdout or ""
|
102
|
-
stderr = completed.stderr or ""
|
103
|
-
if completed.returncode != 0:
|
104
|
-
raise CodexProcessError(
|
105
|
-
returncode=completed.returncode,
|
106
|
-
cmd=tuple(cmd),
|
107
|
-
stdout=stdout,
|
108
|
-
stderr=stderr,
|
109
|
-
)
|
110
|
-
return stdout
|
32
|
+
def __iter__(self) -> Iterator[Event]:
|
33
|
+
"""Yield `Event` objects from the native stream."""
|
34
|
+
for item in self._stream:
|
35
|
+
yield Event.model_validate(item)
|
111
36
|
|
112
37
|
|
113
38
|
@dataclass(slots=True)
|
114
39
|
class CodexClient:
|
115
|
-
"""Lightweight, synchronous client for the Codex
|
40
|
+
"""Lightweight, synchronous client for the native Codex core.
|
116
41
|
|
117
|
-
Provides defaults for repeated invocations and
|
42
|
+
Provides defaults for repeated invocations and conversation management.
|
118
43
|
"""
|
119
44
|
|
120
|
-
|
121
|
-
|
122
|
-
full_auto: bool = False
|
123
|
-
cd: str | None = None
|
45
|
+
config: CodexConfig | None = None
|
46
|
+
load_default_config: bool = True
|
124
47
|
env: Mapping[str, str] | None = None
|
125
48
|
extra_args: Sequence[str] | None = None
|
126
49
|
|
127
|
-
def
|
128
|
-
"""Return the resolved binary path or raise CodexNotFoundError."""
|
129
|
-
return find_binary(self.executable)
|
130
|
-
|
131
|
-
def run(
|
50
|
+
def start_conversation(
|
132
51
|
self,
|
133
52
|
prompt: str,
|
134
53
|
*,
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
eff_extra.extend(list(extra_args))
|
169
|
-
|
170
|
-
return run_exec(
|
54
|
+
config: CodexConfig | None = None,
|
55
|
+
load_default_config: bool | None = None,
|
56
|
+
) -> Conversation:
|
57
|
+
"""Start a new conversation and return a streaming iterator over events."""
|
58
|
+
eff_config = config if config is not None else self.config
|
59
|
+
eff_load_default_config = (
|
60
|
+
load_default_config if load_default_config is not None else self.load_default_config
|
61
|
+
)
|
62
|
+
|
63
|
+
try:
|
64
|
+
stream = native_start_exec_stream(
|
65
|
+
prompt,
|
66
|
+
config_overrides=eff_config.to_dict() if eff_config else None,
|
67
|
+
load_default_config=eff_load_default_config,
|
68
|
+
)
|
69
|
+
return Conversation(_stream=stream)
|
70
|
+
except RuntimeError as e:
|
71
|
+
raise CodexNativeError() from e
|
72
|
+
|
73
|
+
|
74
|
+
def run_exec(
|
75
|
+
prompt: str,
|
76
|
+
*,
|
77
|
+
config: CodexConfig | None = None,
|
78
|
+
load_default_config: bool = True,
|
79
|
+
) -> list[Event]:
|
80
|
+
"""
|
81
|
+
Run a prompt through the native Codex engine and return a list of events.
|
82
|
+
|
83
|
+
- Raises CodexNativeError if the native extension is unavailable or fails.
|
84
|
+
"""
|
85
|
+
try:
|
86
|
+
events = native_run_exec_collect(
|
171
87
|
prompt,
|
172
|
-
|
173
|
-
|
174
|
-
full_auto=eff_full_auto,
|
175
|
-
cd=eff_cd,
|
176
|
-
skip_git_repo_check=eff_skip_git,
|
177
|
-
timeout=timeout,
|
178
|
-
env=merged_env,
|
179
|
-
executable=self.executable,
|
180
|
-
extra_args=eff_extra,
|
88
|
+
config_overrides=config.to_dict() if config else None,
|
89
|
+
load_default_config=load_default_config,
|
181
90
|
)
|
91
|
+
return [Event.model_validate(e) for e in events]
|
92
|
+
except RuntimeError as e:
|
93
|
+
raise CodexNativeError() from e
|
codex/config.py
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Any, cast
|
5
|
+
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
7
|
+
|
8
|
+
|
9
|
+
class ApprovalPolicy(str, Enum):
|
10
|
+
"""Approval policy for executing shell commands.
|
11
|
+
|
12
|
+
Matches Rust enum `AskForApproval` (serde kebab-case):
|
13
|
+
- "untrusted": auto-approve safe read-only commands, ask otherwise
|
14
|
+
- "on-failure": sandbox by default; ask only if the sandboxed run fails
|
15
|
+
- "on-request": model decides (default)
|
16
|
+
- "never": never ask the user
|
17
|
+
"""
|
18
|
+
|
19
|
+
UNTRUSTED = "untrusted"
|
20
|
+
ON_FAILURE = "on-failure"
|
21
|
+
ON_REQUEST = "on-request"
|
22
|
+
NEVER = "never"
|
23
|
+
|
24
|
+
|
25
|
+
class SandboxMode(str, Enum):
|
26
|
+
"""High-level sandbox mode override.
|
27
|
+
|
28
|
+
Matches Rust enum `SandboxMode` (serde kebab-case):
|
29
|
+
- "read-only"
|
30
|
+
- "workspace-write"
|
31
|
+
- "danger-full-access"
|
32
|
+
"""
|
33
|
+
|
34
|
+
READ_ONLY = "read-only"
|
35
|
+
WORKSPACE_WRITE = "workspace-write"
|
36
|
+
DANGER_FULL_ACCESS = "danger-full-access"
|
37
|
+
|
38
|
+
|
39
|
+
class CodexConfig(BaseModel):
|
40
|
+
"""Configuration overrides for Codex.
|
41
|
+
|
42
|
+
This mirrors `codex_core::config::ConfigOverrides` and is intentionally
|
43
|
+
conservative: only values present (not None) are passed to the native core.
|
44
|
+
"""
|
45
|
+
|
46
|
+
# Model selection
|
47
|
+
model: str | None = Field(default=None, description="Model slug, e.g. 'gpt-5' or 'o3'.")
|
48
|
+
model_provider: str | None = Field(
|
49
|
+
default=None, description="Provider key from config, e.g. 'openai'."
|
50
|
+
)
|
51
|
+
|
52
|
+
# Safety/Execution
|
53
|
+
approval_policy: ApprovalPolicy | None = Field(default=None)
|
54
|
+
sandbox_mode: SandboxMode | None = Field(default=None)
|
55
|
+
|
56
|
+
# Environment
|
57
|
+
cwd: str | None = Field(default=None, description="Working directory for the session.")
|
58
|
+
config_profile: str | None = Field(
|
59
|
+
default=None, description="Config profile key to use (from profiles.*)."
|
60
|
+
)
|
61
|
+
codex_linux_sandbox_exe: str | None = Field(
|
62
|
+
default=None, description="Absolute path to codex-linux-sandbox (Linux only)."
|
63
|
+
)
|
64
|
+
|
65
|
+
# UX / features
|
66
|
+
base_instructions: str | None = Field(default=None, description="Override base instructions.")
|
67
|
+
include_plan_tool: bool | None = Field(default=None)
|
68
|
+
include_apply_patch_tool: bool | None = Field(default=None)
|
69
|
+
include_view_image_tool: bool | None = Field(default=None)
|
70
|
+
show_raw_agent_reasoning: bool | None = Field(default=None)
|
71
|
+
tools_web_search_request: bool | None = Field(default=None)
|
72
|
+
|
73
|
+
def to_dict(self) -> dict[str, Any]:
|
74
|
+
"""Return overrides as a plain dict with None values removed.
|
75
|
+
|
76
|
+
Enum fields are emitted as their string values.
|
77
|
+
"""
|
78
|
+
return cast(dict[str, Any], self.model_dump(exclude_none=True))
|
79
|
+
|
80
|
+
# Pydantic v2 config. `use_enum_values=True` ensures enums dump as strings.
|
81
|
+
# Place at end of class, extra='allow' per style.
|
82
|
+
model_config = ConfigDict(extra="allow", validate_assignment=True, use_enum_values=True)
|
codex/event.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
from pydantic.config import ConfigDict
|
5
|
+
|
6
|
+
from .protocol.types import EventMsg
|
7
|
+
|
8
|
+
|
9
|
+
class Event(BaseModel):
|
10
|
+
"""Protocol event envelope with typed `msg` (union of EventMsg_*)."""
|
11
|
+
|
12
|
+
id: str
|
13
|
+
msg: EventMsg
|
14
|
+
|
15
|
+
# Allow forward compatibility with additional envelope fields
|
16
|
+
model_config = ConfigDict(extra="allow")
|
codex/native.py
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
from typing import Any, cast
|
2
|
+
|
3
|
+
try:
|
4
|
+
from codex_native import preview_config as _preview_config
|
5
|
+
from codex_native import run_exec_collect as _run_exec_collect
|
6
|
+
from codex_native import start_exec_stream as _start_exec_stream
|
7
|
+
except Exception as _e: # pragma: no cover - optional native path
|
8
|
+
_run_exec_collect = None
|
9
|
+
_start_exec_stream = None
|
10
|
+
_preview_config = None
|
11
|
+
|
12
|
+
|
13
|
+
def run_exec_collect(
|
14
|
+
prompt: str,
|
15
|
+
*,
|
16
|
+
config_overrides: dict[str, Any] | None = None,
|
17
|
+
load_default_config: bool = True,
|
18
|
+
) -> list[dict]:
|
19
|
+
"""Run Codex natively (in‑process) and return a list of events as dicts.
|
20
|
+
|
21
|
+
Requires the native extension to be built/installed (see `make dev-native`).
|
22
|
+
Falls back to raising if the extension is not available.
|
23
|
+
"""
|
24
|
+
if _run_exec_collect is None:
|
25
|
+
raise RuntimeError(
|
26
|
+
"codex_native extension not installed. Run `make dev-native` or build wheels via maturin."
|
27
|
+
)
|
28
|
+
return cast(list[dict], _run_exec_collect(prompt, config_overrides, load_default_config))
|
29
|
+
|
30
|
+
|
31
|
+
def start_exec_stream(
|
32
|
+
prompt: str,
|
33
|
+
*,
|
34
|
+
config_overrides: dict[str, Any] | None = None,
|
35
|
+
load_default_config: bool = True,
|
36
|
+
) -> Any:
|
37
|
+
"""Return a native streaming iterator over Codex events (dicts)."""
|
38
|
+
if _start_exec_stream is None:
|
39
|
+
raise RuntimeError(
|
40
|
+
"codex_native extension not installed. Run `make dev-native` or build wheels via maturin."
|
41
|
+
)
|
42
|
+
return _start_exec_stream(prompt, config_overrides, load_default_config)
|
43
|
+
|
44
|
+
|
45
|
+
def preview_config(
|
46
|
+
*, config_overrides: dict[str, Any] | None = None, load_default_config: bool = True
|
47
|
+
) -> dict:
|
48
|
+
"""Return an effective config snapshot (selected fields) from native.
|
49
|
+
|
50
|
+
Useful for tests to validate override mapping without running Codex.
|
51
|
+
"""
|
52
|
+
if _preview_config is None: # pragma: no cover
|
53
|
+
raise RuntimeError(
|
54
|
+
"codex_native extension not installed. Run `make dev-native` or build wheels via maturin."
|
55
|
+
)
|
56
|
+
return cast(dict, _preview_config(config_overrides, load_default_config))
|