codex-python 0.1.1__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 CHANGED
@@ -4,27 +4,31 @@ Python interface for the Codex CLI.
4
4
 
5
5
  Usage:
6
6
  from codex import run_exec
7
- output = run_exec("explain this codebase to me")
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
- CodexNotFoundError,
14
- CodexProcessError,
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
- "CodexNotFoundError",
23
- "CodexProcessError",
24
+ "CodexNativeError",
24
25
  "CodexClient",
25
- "find_binary",
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.1.1"
34
+ __version__ = "0.2.0"
codex/api.py CHANGED
@@ -1,165 +1,93 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
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 CodexNotFoundError(CodexError):
15
- """Raised when the 'codex' binary cannot be found or executed."""
16
+ class CodexNativeError(CodexError):
17
+ """Raised when the native extension is not available or fails."""
16
18
 
17
- def __init__(self, executable: str = "codex") -> None:
19
+ def __init__(self) -> None:
18
20
  super().__init__(
19
- f"Codex CLI not found: '{executable}'.\n"
20
- "Install from https://github.com/openai/codex or ensure it is on PATH."
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 CodexProcessError(CodexError):
27
- """Raised when the codex process exits with a non‑zero status."""
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
- def find_binary(executable: str = "codex") -> str:
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
- def run_exec(
51
- prompt: str,
52
- *,
53
- model: str | None = None,
54
- full_auto: bool = False,
55
- cd: str | None = None,
56
- timeout: float | None = None,
57
- env: Mapping[str, str] | None = None,
58
- executable: str = "codex",
59
- extra_args: Iterable[str] | None = None,
60
- ) -> str:
61
- """
62
- Run `codex exec` with the given prompt and return stdout as text.
63
-
64
- - Raises CodexNotFoundError if the binary is unavailable.
65
- - Raises CodexProcessError on non‑zero exit with captured stdout/stderr.
66
- """
67
- bin_path = find_binary(executable)
68
-
69
- cmd: list[str] = [bin_path]
70
-
71
- if cd:
72
- cmd.extend(["--cd", cd])
73
- if model:
74
- cmd.extend(["-m", model])
75
- if full_auto:
76
- cmd.append("--full-auto")
77
- if extra_args:
78
- cmd.extend(list(extra_args))
79
-
80
- cmd.extend(["exec", prompt])
81
-
82
- completed = subprocess.run(
83
- cmd,
84
- capture_output=True,
85
- text=True,
86
- timeout=timeout,
87
- env={**os.environ, **(dict(env) if env else {})},
88
- check=False,
89
- )
90
-
91
- stdout = completed.stdout or ""
92
- stderr = completed.stderr or ""
93
- if completed.returncode != 0:
94
- raise CodexProcessError(
95
- returncode=completed.returncode,
96
- cmd=tuple(cmd),
97
- stdout=stdout,
98
- stderr=stderr,
99
- )
100
- 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)
101
36
 
102
37
 
103
38
  @dataclass(slots=True)
104
39
  class CodexClient:
105
- """Lightweight, synchronous client for the Codex CLI.
40
+ """Lightweight, synchronous client for the native Codex core.
106
41
 
107
- Provides defaults for repeated invocations and convenience helpers.
42
+ Provides defaults for repeated invocations and conversation management.
108
43
  """
109
44
 
110
- executable: str = "codex"
111
- model: str | None = None
112
- full_auto: bool = False
113
- cd: str | None = None
45
+ config: CodexConfig | None = None
46
+ load_default_config: bool = True
114
47
  env: Mapping[str, str] | None = None
115
48
  extra_args: Sequence[str] | None = None
116
49
 
117
- def ensure_available(self) -> str:
118
- """Return the resolved binary path or raise CodexNotFoundError."""
119
- return find_binary(self.executable)
120
-
121
- def run(
50
+ def start_conversation(
122
51
  self,
123
52
  prompt: str,
124
53
  *,
125
- model: str | None = None,
126
- full_auto: bool | None = None,
127
- cd: str | None = None,
128
- timeout: float | None = None,
129
- env: Mapping[str, str] | None = None,
130
- extra_args: Iterable[str] | None = None,
131
- ) -> str:
132
- """Execute `codex exec` and return stdout.
133
-
134
- Explicit arguments override the client's defaults.
135
- """
136
- eff_model = model if model is not None else self.model
137
- eff_full_auto = full_auto if full_auto is not None else self.full_auto
138
- eff_cd = cd if cd is not None else self.cd
139
-
140
- # Merge environment overlays; run_exec will merge with os.environ
141
- merged_env: Mapping[str, str] | None
142
- if self.env and env:
143
- tmp = dict(self.env)
144
- tmp.update(env)
145
- merged_env = tmp
146
- else:
147
- merged_env = env or self.env
148
-
149
- # Compose extra args
150
- eff_extra: list[str] = []
151
- if self.extra_args:
152
- eff_extra.extend(self.extra_args)
153
- if extra_args:
154
- eff_extra.extend(list(extra_args))
155
-
156
- 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(
157
87
  prompt,
158
- model=eff_model,
159
- full_auto=eff_full_auto,
160
- cd=eff_cd,
161
- timeout=timeout,
162
- env=merged_env,
163
- executable=self.executable,
164
- extra_args=eff_extra,
88
+ config_overrides=config.to_dict() if config else None,
89
+ load_default_config=load_default_config,
165
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))