mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Publish Python SDK with Codex-pinned versioning (#18996)
**note**: a large chunk of this diff comes from regenerating Python types after app-server schema changes on `main`. This is PR 3 of 3 for the Python SDK PyPI publishing split. PR #18862 refreshed the generated SDK surface, and PR #18865 made the runtime package publishable as `openai-codex-cli-bin`; this final PR makes the SDK package publishable as `openai-codex-app-server-sdk` and pins both packages to the same Codex runtime version. The key idea is that the published SDK version is the Codex runtime version. That one version now drives the SDK package version, the exact runtime dependency, the client version reported by the SDK, and the bootstrap runtime pin. This keeps release-time versioning in one lane instead of scattering checked-in literals through the package. ## What changed - Rename the SDK distribution from `codex-app-server-sdk` to `openai-codex-app-server-sdk` for conflict-free PyPI publishing. - Use `stage-sdk --codex-version ...` with one Codex version for both the SDK package version and exact `openai-codex-cli-bin` dependency. - Preserve hidden legacy `--runtime-version` / `--sdk-version` args only to reject mismatched versions during staging. - Map PEP 440 package versions back to Codex release tags for runtime setup downloads, e.g. `0.116.0a1` -> `rust-v0.116.0-alpha.1`. - Derive `codex_app_server.__version__`, the default `AppServerConfig.client_version`, and `_runtime_setup.pinned_runtime_version()` from the SDK package/project version instead of hardcoding duplicate version strings. - Carry the current generated SDK refresh from `main` so `generate-types` stays clean after recent app-server schema changes. - Update `sdk/python/uv.lock` for the renamed editable package. ## Validation - `uv run --extra dev pytest` in `sdk/python` -> 59 passed, 37 skipped. - Targeted `uv run ruff check` for the touched SDK files. - `git diff --check`. - Staged runtime with `--codex-version rust-v0.116.0-alpha.1 --platform-tag macosx_11_0_arm64`. - Staged SDK with `--codex-version rust-v0.116.0-alpha.1`. - Built runtime wheel, SDK wheel, and SDK sdist. - `twine check /tmp/codex-python-pr3-build/dist/*` -> passed. - Clean venv smoke installed `openai-codex-app-server-sdk==0.116.0a1` from local dist and pulled `openai-codex-cli-bin==0.116.0a1`. - Smoke imports passed for `Codex` and `bundled_codex_path()`.
This commit is contained in:
committed by
GitHub
Unverified
parent
4ded800374
commit
0f40261e86
@@ -1,6 +1,6 @@
|
||||
# Codex CLI Runtime for Python SDK
|
||||
|
||||
Platform-specific runtime package consumed by the published `codex-app-server-sdk`.
|
||||
Platform-specific runtime package consumed by the published `openai-codex-app-server-sdk`.
|
||||
|
||||
This package is staged during release so the SDK can pin an exact Codex CLI
|
||||
version without checking platform binaries into the repo.
|
||||
|
||||
+20
-14
@@ -12,10 +12,11 @@ uv sync
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Published SDK builds pin an exact `openai-codex-cli-bin` runtime dependency. For local
|
||||
repo development, either pass `AppServerConfig(codex_bin=...)` to point at a
|
||||
local build explicitly, or use the repo examples/notebook bootstrap which
|
||||
installs the pinned runtime package automatically.
|
||||
Published SDK builds pin an exact `openai-codex-cli-bin` runtime dependency
|
||||
with the same version as the SDK. For local repo development, either pass
|
||||
`AppServerConfig(codex_bin=...)` to point at a local build explicitly, or use
|
||||
the repo examples/notebook bootstrap which installs the pinned runtime package
|
||||
automatically.
|
||||
|
||||
## Quickstart
|
||||
|
||||
@@ -54,9 +55,9 @@ python examples/01_quickstart_constructor/async.py
|
||||
|
||||
The repo no longer checks `codex` binaries into `sdk/python`.
|
||||
|
||||
Published SDK builds are pinned to an exact `openai-codex-cli-bin` package version,
|
||||
and that runtime package carries the platform-specific binary for the target
|
||||
wheel.
|
||||
Published SDK builds are pinned to an exact `openai-codex-cli-bin` package
|
||||
version, and that runtime package carries the platform-specific binary for the
|
||||
target wheel. The SDK package version and runtime package version must match.
|
||||
|
||||
For local repo development, the checked-in `sdk/python-runtime` package is only
|
||||
a template for staged release artifacts. Editable installs should use an
|
||||
@@ -70,30 +71,35 @@ cd sdk/python
|
||||
python scripts/update_sdk_artifacts.py generate-types
|
||||
python scripts/update_sdk_artifacts.py \
|
||||
stage-sdk \
|
||||
/tmp/codex-python-release/codex-app-server-sdk \
|
||||
--runtime-version 1.2.3
|
||||
/tmp/codex-python-release/openai-codex-app-server-sdk \
|
||||
--codex-version <codex-release-tag-or-pep440-version>
|
||||
python scripts/update_sdk_artifacts.py \
|
||||
stage-runtime \
|
||||
/tmp/codex-python-release/openai-codex-cli-bin \
|
||||
/path/to/codex \
|
||||
--runtime-version 1.2.3
|
||||
--codex-version <codex-release-tag-or-pep440-version>
|
||||
```
|
||||
|
||||
Pass `--platform-tag ...` to `stage-runtime` when the wheel should be tagged for
|
||||
a Rust target that differs from the Python build host. The intended one-off
|
||||
matrix is `macosx_11_0_arm64`, `macosx_10_9_x86_64`,
|
||||
`musllinux_1_1_aarch64`, `musllinux_1_1_x86_64`, `win_arm64`, and
|
||||
`win_amd64`.
|
||||
|
||||
This supports the CI release flow:
|
||||
|
||||
- run `generate-types` before packaging
|
||||
- stage `codex-app-server-sdk` once with an exact `openai-codex-cli-bin==...` dependency
|
||||
- stage `openai-codex-app-server-sdk` once with an exact `openai-codex-cli-bin==...` dependency
|
||||
- stage `openai-codex-cli-bin` on each supported platform runner with the same pinned runtime version
|
||||
- build and publish `openai-codex-cli-bin` as platform wheels only; do not publish an sdist
|
||||
|
||||
## Compatibility and versioning
|
||||
|
||||
- Package: `codex-app-server-sdk`
|
||||
- Package: `openai-codex-app-server-sdk`
|
||||
- Runtime package: `openai-codex-cli-bin`
|
||||
- Current SDK version in this repo: `0.2.0`
|
||||
- Python: `>=3.10`
|
||||
- Target protocol: Codex `app-server` JSON-RPC v2
|
||||
- Recommendation: keep SDK and `codex` CLI reasonably up to date together
|
||||
- Versioning rule: the SDK package version is the underlying Codex runtime version
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -16,7 +18,7 @@ import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
PACKAGE_NAME = "openai-codex-cli-bin"
|
||||
PINNED_RUNTIME_VERSION = "0.116.0-alpha.1"
|
||||
SDK_PACKAGE_NAME = "openai-codex-app-server-sdk"
|
||||
REPO_SLUG = "openai/codex"
|
||||
|
||||
|
||||
@@ -25,7 +27,16 @@ class RuntimeSetupError(RuntimeError):
|
||||
|
||||
|
||||
def pinned_runtime_version() -> str:
|
||||
return PINNED_RUNTIME_VERSION
|
||||
source_version = _source_tree_project_version()
|
||||
if source_version is not None:
|
||||
return _normalized_package_version(source_version)
|
||||
|
||||
try:
|
||||
return _normalized_package_version(importlib.metadata.version(SDK_PACKAGE_NAME))
|
||||
except importlib.metadata.PackageNotFoundError as exc:
|
||||
raise RuntimeSetupError(
|
||||
f"Unable to resolve {SDK_PACKAGE_NAME} version for runtime pinning."
|
||||
) from exc
|
||||
|
||||
|
||||
def ensure_runtime_package_installed(
|
||||
@@ -39,7 +50,10 @@ def ensure_runtime_package_installed(
|
||||
installed_version = _installed_runtime_version(python_executable)
|
||||
normalized_requested = _normalized_package_version(requested_version)
|
||||
|
||||
if installed_version is not None and _normalized_package_version(installed_version) == normalized_requested:
|
||||
if (
|
||||
installed_version is not None
|
||||
and _normalized_package_version(installed_version) == normalized_requested
|
||||
):
|
||||
return requested_version
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="codex-python-runtime-") as temp_root_str:
|
||||
@@ -61,7 +75,10 @@ def ensure_runtime_package_installed(
|
||||
importlib.invalidate_caches()
|
||||
|
||||
installed_version = _installed_runtime_version(python_executable)
|
||||
if installed_version is None or _normalized_package_version(installed_version) != normalized_requested:
|
||||
if (
|
||||
installed_version is None
|
||||
or _normalized_package_version(installed_version) != normalized_requested
|
||||
):
|
||||
raise RuntimeSetupError(
|
||||
f"Expected {PACKAGE_NAME} {requested_version} in {python_executable}, "
|
||||
f"but found {installed_version!r} after installation."
|
||||
@@ -121,7 +138,8 @@ def _installed_runtime_version(python_executable: str | Path) -> str | None:
|
||||
|
||||
|
||||
def _release_metadata(version: str) -> dict[str, object]:
|
||||
url = f"https://api.github.com/repos/{REPO_SLUG}/releases/tags/rust-v{version}"
|
||||
release_tag = _release_tag(version)
|
||||
url = f"https://api.github.com/repos/{REPO_SLUG}/releases/tags/{release_tag}"
|
||||
token = _github_token()
|
||||
attempts = [True, False] if token is not None else [False]
|
||||
last_error: urllib.error.HTTPError | None = None
|
||||
@@ -146,7 +164,7 @@ def _release_metadata(version: str) -> dict[str, object]:
|
||||
|
||||
assert last_error is not None
|
||||
raise RuntimeSetupError(
|
||||
f"Failed to resolve release metadata for rust-v{version} from {REPO_SLUG}: "
|
||||
f"Failed to resolve release metadata for {release_tag} from {REPO_SLUG}: "
|
||||
f"{last_error.code} {last_error.reason}"
|
||||
) from last_error
|
||||
|
||||
@@ -154,9 +172,10 @@ def _release_metadata(version: str) -> dict[str, object]:
|
||||
def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
asset_name = platform_asset_name()
|
||||
archive_path = temp_root / asset_name
|
||||
release_tag = _release_tag(version)
|
||||
|
||||
browser_download_url = (
|
||||
f"https://github.com/{REPO_SLUG}/releases/download/rust-v{version}/{asset_name}"
|
||||
f"https://github.com/{REPO_SLUG}/releases/download/{release_tag}/{asset_name}"
|
||||
)
|
||||
request = urllib.request.Request(
|
||||
browser_download_url,
|
||||
@@ -172,7 +191,9 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
metadata = _release_metadata(version)
|
||||
assets = metadata.get("assets")
|
||||
if not isinstance(assets, list):
|
||||
raise RuntimeSetupError(f"Release rust-v{version} returned malformed assets metadata.")
|
||||
raise RuntimeSetupError(
|
||||
f"Release {release_tag} returned malformed assets metadata."
|
||||
)
|
||||
asset = next(
|
||||
(
|
||||
item
|
||||
@@ -183,7 +204,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
)
|
||||
if asset is None:
|
||||
raise RuntimeSetupError(
|
||||
f"Release rust-v{version} does not contain asset {asset_name} for this platform."
|
||||
f"Release {release_tag} does not contain asset {asset_name} for this platform."
|
||||
)
|
||||
|
||||
api_url = asset.get("url")
|
||||
@@ -198,7 +219,10 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
headers=_github_api_headers("application/octet-stream"),
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request) as response, archive_path.open("wb") as fh:
|
||||
with (
|
||||
urllib.request.urlopen(request) as response,
|
||||
archive_path.open("wb") as fh,
|
||||
):
|
||||
shutil.copyfileobj(response, fh)
|
||||
return archive_path
|
||||
except urllib.error.HTTPError:
|
||||
@@ -216,7 +240,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
"gh",
|
||||
"release",
|
||||
"download",
|
||||
f"rust-v{version}",
|
||||
release_tag,
|
||||
"--repo",
|
||||
REPO_SLUG,
|
||||
"--pattern",
|
||||
@@ -230,7 +254,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise RuntimeSetupError(
|
||||
f"gh release download failed for rust-v{version} asset {asset_name}.\n"
|
||||
f"gh release download failed for {release_tag} asset {asset_name}.\n"
|
||||
f"STDOUT:\n{exc.stdout}\nSTDERR:\n{exc.stderr}"
|
||||
) from exc
|
||||
return archive_path
|
||||
@@ -249,7 +273,9 @@ def _extract_runtime_binary(archive_path: Path, temp_root: Path) -> Path:
|
||||
with zipfile.ZipFile(archive_path) as zip_file:
|
||||
zip_file.extractall(extract_dir)
|
||||
else:
|
||||
raise RuntimeSetupError(f"Unsupported release archive format: {archive_path.name}")
|
||||
raise RuntimeSetupError(
|
||||
f"Unsupported release archive format: {archive_path.name}"
|
||||
)
|
||||
|
||||
binary_name = runtime_binary_name()
|
||||
archive_stem = archive_path.name.removesuffix(".tar.gz").removesuffix(".zip")
|
||||
@@ -346,12 +372,50 @@ def _github_token() -> str | None:
|
||||
|
||||
|
||||
def _normalized_package_version(version: str) -> str:
|
||||
return version.strip().replace("-alpha.", "a").replace("-beta.", "b")
|
||||
normalized = version.strip()
|
||||
if normalized.startswith("rust-v"):
|
||||
normalized = normalized.removeprefix("rust-v")
|
||||
elif normalized.startswith("v"):
|
||||
normalized = normalized.removeprefix("v")
|
||||
|
||||
normalized = re.sub(r"-alpha\.?([0-9]+)$", r"a\1", normalized)
|
||||
normalized = re.sub(r"-beta\.?([0-9]+)$", r"b\1", normalized)
|
||||
normalized = re.sub(r"-rc\.?([0-9]+)$", r"rc\1", normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def _codex_release_version(version: str) -> str:
|
||||
normalized = _normalized_package_version(version)
|
||||
match = re.fullmatch(r"([0-9]+(?:\.[0-9]+)*)(a|b|rc)([0-9]+)", normalized)
|
||||
if match is None:
|
||||
return normalized
|
||||
|
||||
base, prerelease, number = match.groups()
|
||||
prerelease_name = {"a": "alpha", "b": "beta", "rc": "rc"}[prerelease]
|
||||
return f"{base}-{prerelease_name}.{number}"
|
||||
|
||||
|
||||
def _release_tag(version: str) -> str:
|
||||
return f"rust-v{_codex_release_version(version)}"
|
||||
|
||||
|
||||
def _source_tree_project_version() -> str | None:
|
||||
pyproject_path = Path(__file__).resolve().parent / "pyproject.toml"
|
||||
if not pyproject_path.exists():
|
||||
return None
|
||||
|
||||
match = re.search(
|
||||
r'(?m)^version = "([^"]+)"$',
|
||||
pyproject_path.read_text(encoding="utf-8"),
|
||||
)
|
||||
if match is None:
|
||||
return None
|
||||
return match.group(1)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PACKAGE_NAME",
|
||||
"PINNED_RUNTIME_VERSION",
|
||||
"SDK_PACKAGE_NAME",
|
||||
"RuntimeSetupError",
|
||||
"ensure_runtime_package_installed",
|
||||
"pinned_runtime_version",
|
||||
|
||||
+10
-5
@@ -60,23 +60,28 @@ Common causes:
|
||||
- incompatible/old app-server
|
||||
|
||||
Maintainers stage releases by building the SDK once and the runtime once per
|
||||
platform with the same pinned runtime version. Publish `openai-codex-cli-bin` as
|
||||
platform wheels only; do not publish an sdist:
|
||||
platform with the same pinned runtime version. Publish `openai-codex-cli-bin`
|
||||
as platform wheels only; do not publish an sdist:
|
||||
|
||||
```bash
|
||||
cd sdk/python
|
||||
python scripts/update_sdk_artifacts.py generate-types
|
||||
python scripts/update_sdk_artifacts.py \
|
||||
stage-sdk \
|
||||
/tmp/codex-python-release/codex-app-server-sdk \
|
||||
--runtime-version 1.2.3
|
||||
/tmp/codex-python-release/openai-codex-app-server-sdk \
|
||||
--codex-version <codex-release-tag-or-pep440-version>
|
||||
python scripts/update_sdk_artifacts.py \
|
||||
stage-runtime \
|
||||
/tmp/codex-python-release/openai-codex-cli-bin \
|
||||
/path/to/codex \
|
||||
--runtime-version 1.2.3
|
||||
--codex-version <codex-release-tag-or-pep440-version>
|
||||
```
|
||||
|
||||
If you are packaging a binary for a different target than the Python build
|
||||
host, pass `--platform-tag ...` to `stage-runtime`. The intended one-off matrix
|
||||
is `macosx_11_0_arm64`, `macosx_10_9_x86_64`, `musllinux_1_1_aarch64`,
|
||||
`musllinux_1_1_x86_64`, `win_arm64`, and `win_amd64`.
|
||||
|
||||
## Why does a turn "hang"?
|
||||
|
||||
A turn is complete only when `turn/completed` arrives for that turn ID.
|
||||
|
||||
@@ -28,7 +28,7 @@ will download the matching GitHub release artifact, stage a temporary local
|
||||
`openai-codex-cli-bin` package, install it into your active interpreter, and clean up
|
||||
the temporary files afterward.
|
||||
|
||||
Current pinned runtime version: `0.116.0-alpha.1`
|
||||
The pinned runtime version comes from the SDK package version.
|
||||
|
||||
## Run examples
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ requires = ["hatchling>=1.24.0"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "codex-app-server-sdk"
|
||||
version = "0.2.0"
|
||||
name = "openai-codex-app-server-sdk"
|
||||
version = "0.116.0a1"
|
||||
description = "Python SDK for Codex app-server v2"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -17,6 +17,7 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Sequence, get_args, get_origin
|
||||
|
||||
SDK_DISTRIBUTION_NAME = "openai-codex-app-server-sdk"
|
||||
RUNTIME_DISTRIBUTION_NAME = "openai-codex-cli-bin"
|
||||
|
||||
|
||||
@@ -178,15 +179,19 @@ def _rewrite_sdk_runtime_dependency(pyproject_text: str, runtime_version: str) -
|
||||
)
|
||||
|
||||
raw_items = [item.strip() for item in match.group(1).split(",") if item.strip()]
|
||||
raw_items = [item for item in raw_items if "codex-cli-bin" not in item]
|
||||
raw_items = [
|
||||
item
|
||||
for item in raw_items
|
||||
if RUNTIME_DISTRIBUTION_NAME.removeprefix("openai-") not in item
|
||||
and RUNTIME_DISTRIBUTION_NAME not in item
|
||||
]
|
||||
raw_items.append(f'"{RUNTIME_DISTRIBUTION_NAME}=={runtime_version}"')
|
||||
replacement = "dependencies = [\n " + ",\n ".join(raw_items) + ",\n]"
|
||||
return pyproject_text[: match.start()] + replacement + pyproject_text[match.end() :]
|
||||
|
||||
|
||||
def stage_python_sdk_package(
|
||||
staging_dir: Path, sdk_version: str, runtime_version: str
|
||||
) -> Path:
|
||||
def stage_python_sdk_package(staging_dir: Path, codex_version: str) -> Path:
|
||||
package_version = normalize_codex_version(codex_version)
|
||||
_copy_package_tree(sdk_root(), staging_dir)
|
||||
sdk_bin_dir = staging_dir / "src" / "codex_app_server" / "bin"
|
||||
if sdk_bin_dir.exists():
|
||||
@@ -194,8 +199,9 @@ def stage_python_sdk_package(
|
||||
|
||||
pyproject_path = staging_dir / "pyproject.toml"
|
||||
pyproject_text = pyproject_path.read_text()
|
||||
pyproject_text = _rewrite_project_version(pyproject_text, sdk_version)
|
||||
pyproject_text = _rewrite_sdk_runtime_dependency(pyproject_text, runtime_version)
|
||||
pyproject_text = _rewrite_project_name(pyproject_text, SDK_DISTRIBUTION_NAME)
|
||||
pyproject_text = _rewrite_project_version(pyproject_text, package_version)
|
||||
pyproject_text = _rewrite_sdk_runtime_dependency(pyproject_text, package_version)
|
||||
pyproject_path.write_text(pyproject_text)
|
||||
return staging_dir
|
||||
|
||||
@@ -625,7 +631,7 @@ class PublicFieldSpec:
|
||||
@dataclass(frozen=True)
|
||||
class CliOps:
|
||||
generate_types: Callable[[], None]
|
||||
stage_python_sdk_package: Callable[[Path, str, str], Path]
|
||||
stage_python_sdk_package: Callable[[Path, str], Path]
|
||||
stage_python_runtime_package: Callable[[Path, str, Path, str | None], Path]
|
||||
current_sdk_version: Callable[[], str]
|
||||
|
||||
@@ -992,14 +998,21 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
type=Path,
|
||||
help="Output directory for the staged SDK package",
|
||||
)
|
||||
stage_sdk_parser.add_argument(
|
||||
"--codex-version",
|
||||
help=(
|
||||
"Codex release version to write into the staged SDK package and exact "
|
||||
f"{RUNTIME_DISTRIBUTION_NAME} dependency. Accepts PEP 440 versions "
|
||||
"or release tags such as rust-v0.116.0-alpha.1."
|
||||
),
|
||||
)
|
||||
stage_sdk_parser.add_argument(
|
||||
"--runtime-version",
|
||||
required=True,
|
||||
help="Pinned openai-codex-cli-bin version for the staged SDK package",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
stage_sdk_parser.add_argument(
|
||||
"--sdk-version",
|
||||
help="Version to write into the staged SDK package (defaults to sdk/python current version)",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
stage_runtime_parser = subparsers.add_parser(
|
||||
@@ -1050,22 +1063,23 @@ def default_cli_ops() -> CliOps:
|
||||
)
|
||||
|
||||
|
||||
def _resolve_runtime_version(args: argparse.Namespace) -> str:
|
||||
def _resolve_codex_version(args: argparse.Namespace) -> str:
|
||||
versions = [
|
||||
value
|
||||
for value in (
|
||||
getattr(args, "codex_version", None),
|
||||
getattr(args, "runtime_version", None),
|
||||
getattr(args, "sdk_version", None),
|
||||
)
|
||||
if value is not None
|
||||
]
|
||||
if not versions:
|
||||
raise RuntimeError("Pass --codex-version to stage the Python runtime package")
|
||||
raise RuntimeError("Pass --codex-version to stage Python release artifacts")
|
||||
|
||||
normalized_versions = [normalize_codex_version(version) for version in versions]
|
||||
if len(set(normalized_versions)) != 1:
|
||||
raise RuntimeError(
|
||||
"Runtime package versions must match; pass one --codex-version"
|
||||
"SDK and runtime package versions must match; pass one --codex-version"
|
||||
)
|
||||
return normalized_versions[0]
|
||||
|
||||
@@ -1074,17 +1088,17 @@ def run_command(args: argparse.Namespace, ops: CliOps) -> None:
|
||||
if args.command == "generate-types":
|
||||
ops.generate_types()
|
||||
elif args.command == "stage-sdk":
|
||||
codex_version = _resolve_codex_version(args)
|
||||
ops.generate_types()
|
||||
ops.stage_python_sdk_package(
|
||||
args.staging_dir,
|
||||
args.sdk_version or ops.current_sdk_version(),
|
||||
args.runtime_version,
|
||||
codex_version,
|
||||
)
|
||||
elif args.command == "stage-runtime":
|
||||
runtime_version = _resolve_runtime_version(args)
|
||||
codex_version = _resolve_codex_version(args)
|
||||
ops.stage_python_runtime_package(
|
||||
args.staging_dir,
|
||||
runtime_version,
|
||||
codex_version,
|
||||
args.runtime_binary.resolve(),
|
||||
args.platform_tag,
|
||||
)
|
||||
|
||||
@@ -54,8 +54,7 @@ from .api import (
|
||||
TurnHandle,
|
||||
)
|
||||
from .retry import retry_on_overload
|
||||
|
||||
__version__ = "0.2.0"
|
||||
from ._version import __version__
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from importlib.metadata import PackageNotFoundError
|
||||
from importlib.metadata import version as distribution_version
|
||||
from pathlib import Path
|
||||
|
||||
DISTRIBUTION_NAME = "openai-codex-app-server-sdk"
|
||||
UNKNOWN_VERSION = "0+unknown"
|
||||
|
||||
|
||||
def package_version() -> str:
|
||||
source_version = _source_tree_project_version()
|
||||
if source_version is not None:
|
||||
return source_version
|
||||
|
||||
try:
|
||||
return distribution_version(DISTRIBUTION_NAME)
|
||||
except PackageNotFoundError:
|
||||
return UNKNOWN_VERSION
|
||||
|
||||
|
||||
def _source_tree_project_version() -> str | None:
|
||||
pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
||||
if not pyproject_path.exists():
|
||||
return None
|
||||
|
||||
match = re.search(
|
||||
r'(?m)^version = "([^"]+)"$',
|
||||
pyproject_path.read_text(encoding="utf-8"),
|
||||
)
|
||||
if match is None:
|
||||
return None
|
||||
return match.group(1)
|
||||
|
||||
|
||||
__version__ = package_version()
|
||||
@@ -10,15 +10,18 @@ from .generated.v2_all import (
|
||||
ApprovalsReviewer,
|
||||
AskForApproval,
|
||||
ModelListResponse,
|
||||
PermissionProfile,
|
||||
Personality,
|
||||
ReasoningEffort,
|
||||
ReasoningSummary,
|
||||
SandboxMode,
|
||||
SandboxPolicy,
|
||||
ServiceTier,
|
||||
SortDirection,
|
||||
ThreadArchiveResponse,
|
||||
ThreadCompactStartResponse,
|
||||
ThreadForkParams,
|
||||
ThreadListCwdFilter,
|
||||
ThreadListParams,
|
||||
ThreadListResponse,
|
||||
ThreadReadResponse,
|
||||
@@ -26,6 +29,7 @@ from .generated.v2_all import (
|
||||
ThreadSetNameResponse,
|
||||
ThreadSortKey,
|
||||
ThreadSourceKind,
|
||||
ThreadStartSource,
|
||||
ThreadStartParams,
|
||||
Turn as AppServerTurn,
|
||||
TurnCompletedNotification,
|
||||
@@ -146,6 +150,7 @@ class Codex:
|
||||
ephemeral: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
permission_profile: PermissionProfile | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_name: str | None = None,
|
||||
@@ -162,6 +167,7 @@ class Codex:
|
||||
ephemeral=ephemeral,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
permission_profile=permission_profile,
|
||||
personality=personality,
|
||||
sandbox=sandbox,
|
||||
service_name=service_name,
|
||||
@@ -176,13 +182,14 @@ class Codex:
|
||||
*,
|
||||
archived: bool | None = None,
|
||||
cursor: str | None = None,
|
||||
cwd: str | None = None,
|
||||
cwd: ThreadListCwdFilter | None = None,
|
||||
limit: int | None = None,
|
||||
model_providers: list[str] | None = None,
|
||||
search_term: str | None = None,
|
||||
sort_direction: SortDirection | None = None,
|
||||
sort_key: ThreadSortKey | None = None,
|
||||
source_kinds: list[ThreadSourceKind] | None = None,
|
||||
use_state_db_only: bool | None = None,
|
||||
) -> ThreadListResponse:
|
||||
params = ThreadListParams(
|
||||
archived=archived,
|
||||
@@ -194,6 +201,7 @@ class Codex:
|
||||
sort_direction=sort_direction,
|
||||
sort_key=sort_key,
|
||||
source_kinds=source_kinds,
|
||||
use_state_db_only=use_state_db_only,
|
||||
)
|
||||
return self._client.thread_list(params)
|
||||
|
||||
@@ -207,8 +215,10 @@ class Codex:
|
||||
config: JsonObject | None = None,
|
||||
cwd: str | None = None,
|
||||
developer_instructions: str | None = None,
|
||||
exclude_turns: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
permission_profile: PermissionProfile | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
@@ -221,8 +231,10 @@ class Codex:
|
||||
config=config,
|
||||
cwd=cwd,
|
||||
developer_instructions=developer_instructions,
|
||||
exclude_turns=exclude_turns,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
permission_profile=permission_profile,
|
||||
personality=personality,
|
||||
sandbox=sandbox,
|
||||
service_tier=service_tier,
|
||||
@@ -241,8 +253,10 @@ class Codex:
|
||||
cwd: str | None = None,
|
||||
developer_instructions: str | None = None,
|
||||
ephemeral: bool | None = None,
|
||||
exclude_turns: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
permission_profile: PermissionProfile | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
) -> Thread:
|
||||
@@ -255,8 +269,10 @@ class Codex:
|
||||
cwd=cwd,
|
||||
developer_instructions=developer_instructions,
|
||||
ephemeral=ephemeral,
|
||||
exclude_turns=exclude_turns,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
permission_profile=permission_profile,
|
||||
sandbox=sandbox,
|
||||
service_tier=service_tier,
|
||||
)
|
||||
@@ -340,6 +356,7 @@ class AsyncCodex:
|
||||
ephemeral: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
permission_profile: PermissionProfile | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_name: str | None = None,
|
||||
@@ -357,6 +374,7 @@ class AsyncCodex:
|
||||
ephemeral=ephemeral,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
permission_profile=permission_profile,
|
||||
personality=personality,
|
||||
sandbox=sandbox,
|
||||
service_name=service_name,
|
||||
@@ -371,13 +389,14 @@ class AsyncCodex:
|
||||
*,
|
||||
archived: bool | None = None,
|
||||
cursor: str | None = None,
|
||||
cwd: str | None = None,
|
||||
cwd: ThreadListCwdFilter | None = None,
|
||||
limit: int | None = None,
|
||||
model_providers: list[str] | None = None,
|
||||
search_term: str | None = None,
|
||||
sort_direction: SortDirection | None = None,
|
||||
sort_key: ThreadSortKey | None = None,
|
||||
source_kinds: list[ThreadSourceKind] | None = None,
|
||||
use_state_db_only: bool | None = None,
|
||||
) -> ThreadListResponse:
|
||||
await self._ensure_initialized()
|
||||
params = ThreadListParams(
|
||||
@@ -390,6 +409,7 @@ class AsyncCodex:
|
||||
sort_direction=sort_direction,
|
||||
sort_key=sort_key,
|
||||
source_kinds=source_kinds,
|
||||
use_state_db_only=use_state_db_only,
|
||||
)
|
||||
return await self._client.thread_list(params)
|
||||
|
||||
@@ -403,8 +423,10 @@ class AsyncCodex:
|
||||
config: JsonObject | None = None,
|
||||
cwd: str | None = None,
|
||||
developer_instructions: str | None = None,
|
||||
exclude_turns: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
permission_profile: PermissionProfile | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
@@ -418,8 +440,10 @@ class AsyncCodex:
|
||||
config=config,
|
||||
cwd=cwd,
|
||||
developer_instructions=developer_instructions,
|
||||
exclude_turns=exclude_turns,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
permission_profile=permission_profile,
|
||||
personality=personality,
|
||||
sandbox=sandbox,
|
||||
service_tier=service_tier,
|
||||
@@ -438,8 +462,10 @@ class AsyncCodex:
|
||||
cwd: str | None = None,
|
||||
developer_instructions: str | None = None,
|
||||
ephemeral: bool | None = None,
|
||||
exclude_turns: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
permission_profile: PermissionProfile | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
) -> AsyncThread:
|
||||
@@ -453,8 +479,10 @@ class AsyncCodex:
|
||||
cwd=cwd,
|
||||
developer_instructions=developer_instructions,
|
||||
ephemeral=ephemeral,
|
||||
exclude_turns=exclude_turns,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
permission_profile=permission_profile,
|
||||
sandbox=sandbox,
|
||||
service_tier=service_tier,
|
||||
)
|
||||
@@ -491,6 +519,7 @@ class Thread:
|
||||
effort: ReasoningEffort | None = None,
|
||||
model: str | None = None,
|
||||
output_schema: JsonObject | None = None,
|
||||
permission_profile: PermissionProfile | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
@@ -504,6 +533,7 @@ class Thread:
|
||||
effort=effort,
|
||||
model=model,
|
||||
output_schema=output_schema,
|
||||
permission_profile=permission_profile,
|
||||
personality=personality,
|
||||
sandbox_policy=sandbox_policy,
|
||||
service_tier=service_tier,
|
||||
@@ -526,6 +556,7 @@ class Thread:
|
||||
effort: ReasoningEffort | None = None,
|
||||
model: str | None = None,
|
||||
output_schema: JsonObject | None = None,
|
||||
permission_profile: PermissionProfile | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
@@ -541,6 +572,7 @@ class Thread:
|
||||
effort=effort,
|
||||
model=model,
|
||||
output_schema=output_schema,
|
||||
permission_profile=permission_profile,
|
||||
personality=personality,
|
||||
sandbox_policy=sandbox_policy,
|
||||
service_tier=service_tier,
|
||||
@@ -575,6 +607,7 @@ class AsyncThread:
|
||||
effort: ReasoningEffort | None = None,
|
||||
model: str | None = None,
|
||||
output_schema: JsonObject | None = None,
|
||||
permission_profile: PermissionProfile | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
@@ -588,6 +621,7 @@ class AsyncThread:
|
||||
effort=effort,
|
||||
model=model,
|
||||
output_schema=output_schema,
|
||||
permission_profile=permission_profile,
|
||||
personality=personality,
|
||||
sandbox_policy=sandbox_policy,
|
||||
service_tier=service_tier,
|
||||
@@ -610,6 +644,7 @@ class AsyncThread:
|
||||
effort: ReasoningEffort | None = None,
|
||||
model: str | None = None,
|
||||
output_schema: JsonObject | None = None,
|
||||
permission_profile: PermissionProfile | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
@@ -626,6 +661,7 @@ class AsyncThread:
|
||||
effort=effort,
|
||||
model=model,
|
||||
output_schema=output_schema,
|
||||
permission_profile=permission_profile,
|
||||
personality=personality,
|
||||
sandbox_policy=sandbox_policy,
|
||||
service_tier=service_tier,
|
||||
|
||||
@@ -44,6 +44,7 @@ from .models import (
|
||||
UnknownNotification,
|
||||
)
|
||||
from .retry import retry_on_overload
|
||||
from ._version import __version__ as SDK_VERSION
|
||||
|
||||
ModelT = TypeVar("ModelT", bound=BaseModel)
|
||||
ApprovalHandler = Callable[[str, JsonObject | None], JsonObject]
|
||||
@@ -129,7 +130,7 @@ class AppServerConfig:
|
||||
env: dict[str, str] | None = None
|
||||
client_name: str = "codex_python_sdk"
|
||||
client_title: str = "Codex Python SDK"
|
||||
client_version: str = "0.2.0"
|
||||
client_version: str = SDK_VERSION
|
||||
experimental_api: bool = True
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from .v2_all import FileChangePatchUpdatedNotification
|
||||
from .v2_all import FsChangedNotification
|
||||
from .v2_all import FuzzyFileSearchSessionCompletedNotification
|
||||
from .v2_all import FuzzyFileSearchSessionUpdatedNotification
|
||||
from .v2_all import GuardianWarningNotification
|
||||
from .v2_all import HookCompletedNotification
|
||||
from .v2_all import HookStartedNotification
|
||||
from .v2_all import ItemCompletedNotification
|
||||
@@ -32,6 +33,7 @@ from .v2_all import McpServerOauthLoginCompletedNotification
|
||||
from .v2_all import McpServerStatusUpdatedNotification
|
||||
from .v2_all import McpToolCallProgressNotification
|
||||
from .v2_all import ModelReroutedNotification
|
||||
from .v2_all import ModelVerificationNotification
|
||||
from .v2_all import PlanDeltaNotification
|
||||
from .v2_all import ReasoningSummaryPartAddedNotification
|
||||
from .v2_all import ReasoningSummaryTextDeltaNotification
|
||||
@@ -41,6 +43,8 @@ from .v2_all import SkillsChangedNotification
|
||||
from .v2_all import TerminalInteractionNotification
|
||||
from .v2_all import ThreadArchivedNotification
|
||||
from .v2_all import ThreadClosedNotification
|
||||
from .v2_all import ThreadGoalClearedNotification
|
||||
from .v2_all import ThreadGoalUpdatedNotification
|
||||
from .v2_all import ThreadNameUpdatedNotification
|
||||
from .v2_all import ThreadRealtimeClosedNotification
|
||||
from .v2_all import ThreadRealtimeErrorNotification
|
||||
@@ -75,6 +79,7 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = {
|
||||
"fs/changed": FsChangedNotification,
|
||||
"fuzzyFileSearch/sessionCompleted": FuzzyFileSearchSessionCompletedNotification,
|
||||
"fuzzyFileSearch/sessionUpdated": FuzzyFileSearchSessionUpdatedNotification,
|
||||
"guardianWarning": GuardianWarningNotification,
|
||||
"hook/completed": HookCompletedNotification,
|
||||
"hook/started": HookStartedNotification,
|
||||
"item/agentMessage/delta": AgentMessageDeltaNotification,
|
||||
@@ -94,11 +99,14 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = {
|
||||
"mcpServer/oauthLogin/completed": McpServerOauthLoginCompletedNotification,
|
||||
"mcpServer/startupStatus/updated": McpServerStatusUpdatedNotification,
|
||||
"model/rerouted": ModelReroutedNotification,
|
||||
"model/verification": ModelVerificationNotification,
|
||||
"serverRequest/resolved": ServerRequestResolvedNotification,
|
||||
"skills/changed": SkillsChangedNotification,
|
||||
"thread/archived": ThreadArchivedNotification,
|
||||
"thread/closed": ThreadClosedNotification,
|
||||
"thread/compacted": ContextCompactedNotification,
|
||||
"thread/goal/cleared": ThreadGoalClearedNotification,
|
||||
"thread/goal/updated": ThreadGoalUpdatedNotification,
|
||||
"thread/name/updated": ThreadNameUpdatedNotification,
|
||||
"thread/realtime/closed": ThreadRealtimeClosedNotification,
|
||||
"thread/realtime/error": ThreadRealtimeErrorNotification,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,9 @@ def _load_runtime_setup_module():
|
||||
runtime_setup_path = ROOT / "_runtime_setup.py"
|
||||
spec = importlib.util.spec_from_file_location("_runtime_setup", runtime_setup_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise AssertionError(f"Failed to load runtime setup module: {runtime_setup_path}")
|
||||
raise AssertionError(
|
||||
f"Failed to load runtime setup module: {runtime_setup_path}"
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
@@ -159,29 +161,32 @@ def test_runtime_package_template_has_no_checked_in_binaries() -> None:
|
||||
) == ["__init__.py"]
|
||||
|
||||
|
||||
def test_examples_readme_matches_pinned_runtime_version() -> None:
|
||||
runtime_setup = _load_runtime_setup_module()
|
||||
def test_examples_readme_points_to_runtime_version_source_of_truth() -> None:
|
||||
readme = (ROOT / "examples" / "README.md").read_text()
|
||||
assert (
|
||||
f"Current pinned runtime version: `{runtime_setup.pinned_runtime_version()}`"
|
||||
in readme
|
||||
)
|
||||
assert "The pinned runtime version comes from the SDK package version." in readme
|
||||
|
||||
|
||||
def test_runtime_distribution_name_is_consistent() -> None:
|
||||
script = _load_update_script_module()
|
||||
runtime_setup = _load_runtime_setup_module()
|
||||
from codex_app_server import client as client_module
|
||||
from codex_app_server import _version
|
||||
|
||||
assert script.SDK_DISTRIBUTION_NAME == "openai-codex-app-server-sdk"
|
||||
assert runtime_setup.SDK_PACKAGE_NAME == "openai-codex-app-server-sdk"
|
||||
assert _version.DISTRIBUTION_NAME == "openai-codex-app-server-sdk"
|
||||
assert script.RUNTIME_DISTRIBUTION_NAME == "openai-codex-cli-bin"
|
||||
assert runtime_setup.PACKAGE_NAME == "openai-codex-cli-bin"
|
||||
assert client_module.RUNTIME_PKG_NAME == "openai-codex-cli-bin"
|
||||
assert "importlib.metadata.version('codex-cli-bin')" not in (
|
||||
ROOT / "_runtime_setup.py"
|
||||
).read_text()
|
||||
assert (
|
||||
"importlib.metadata.version('codex-cli-bin')"
|
||||
not in (ROOT / "_runtime_setup.py").read_text()
|
||||
)
|
||||
|
||||
|
||||
def test_release_metadata_retries_without_invalid_auth(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_release_metadata_retries_without_invalid_auth(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime_setup = _load_runtime_setup_module()
|
||||
authorizations: list[str | None] = []
|
||||
|
||||
@@ -205,6 +210,19 @@ def test_release_metadata_retries_without_invalid_auth(monkeypatch: pytest.Monke
|
||||
assert authorizations == ["Bearer invalid-token", None]
|
||||
|
||||
|
||||
def test_runtime_setup_uses_pep440_package_version_and_codex_release_tags() -> None:
|
||||
runtime_setup = _load_runtime_setup_module()
|
||||
pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text())
|
||||
|
||||
assert runtime_setup.PACKAGE_NAME == "openai-codex-cli-bin"
|
||||
assert runtime_setup.pinned_runtime_version() == pyproject["project"]["version"]
|
||||
assert (
|
||||
runtime_setup._normalized_package_version("rust-v0.116.0-alpha.1")
|
||||
== "0.116.0a1"
|
||||
)
|
||||
assert runtime_setup._release_tag("0.116.0a1") == "rust-v0.116.0-alpha.1"
|
||||
|
||||
|
||||
def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() -> None:
|
||||
pyproject = tomllib.loads(
|
||||
(ROOT.parent / "python-runtime" / "pyproject.toml").read_text()
|
||||
@@ -334,12 +352,23 @@ def test_stage_runtime_release_can_pin_wheel_platform_tag(tmp_path: Path) -> Non
|
||||
|
||||
def test_stage_sdk_release_injects_exact_runtime_pin(tmp_path: Path) -> None:
|
||||
script = _load_update_script_module()
|
||||
staged = script.stage_python_sdk_package(tmp_path / "sdk-stage", "0.2.1", "1.2.3")
|
||||
staged = script.stage_python_sdk_package(
|
||||
tmp_path / "sdk-stage",
|
||||
"rust-v0.116.0-alpha.1",
|
||||
)
|
||||
|
||||
pyproject = (staged / "pyproject.toml").read_text()
|
||||
assert 'version = "0.2.1"' in pyproject
|
||||
assert '"openai-codex-cli-bin==1.2.3"' in pyproject
|
||||
assert '"codex-cli-bin==1.2.3"' not in pyproject
|
||||
assert 'name = "openai-codex-app-server-sdk"' in pyproject
|
||||
assert 'version = "0.116.0a1"' in pyproject
|
||||
assert '"openai-codex-cli-bin==0.116.0a1"' in pyproject
|
||||
assert (
|
||||
'__version__ = "0.116.0a1"'
|
||||
not in (staged / "src" / "codex_app_server" / "__init__.py").read_text()
|
||||
)
|
||||
assert (
|
||||
'client_version: str = "0.116.0a1"'
|
||||
not in (staged / "src" / "codex_app_server" / "client.py").read_text()
|
||||
)
|
||||
assert not any((staged / "src" / "codex_app_server").glob("bin/**"))
|
||||
|
||||
|
||||
@@ -350,12 +379,39 @@ def test_stage_sdk_release_replaces_existing_staging_dir(tmp_path: Path) -> None
|
||||
old_file.parent.mkdir(parents=True)
|
||||
old_file.write_text("stale")
|
||||
|
||||
staged = script.stage_python_sdk_package(staging_dir, "0.2.1", "1.2.3")
|
||||
staged = script.stage_python_sdk_package(staging_dir, "0.116.0a1")
|
||||
|
||||
assert staged == staging_dir
|
||||
assert not old_file.exists()
|
||||
|
||||
|
||||
def test_staged_sdk_and_runtime_versions_match(tmp_path: Path) -> None:
|
||||
script = _load_update_script_module()
|
||||
fake_binary = tmp_path / script.runtime_binary_name()
|
||||
fake_binary.write_text("fake codex\n")
|
||||
|
||||
sdk_stage = script.stage_python_sdk_package(
|
||||
tmp_path / "sdk-stage",
|
||||
"rust-v0.116.0-alpha.1",
|
||||
)
|
||||
runtime_stage = script.stage_python_runtime_package(
|
||||
tmp_path / "runtime-stage",
|
||||
"rust-v0.116.0-alpha.1",
|
||||
fake_binary,
|
||||
)
|
||||
|
||||
sdk_pyproject = tomllib.loads((sdk_stage / "pyproject.toml").read_text())
|
||||
runtime_pyproject = tomllib.loads((runtime_stage / "pyproject.toml").read_text())
|
||||
|
||||
assert (
|
||||
sdk_pyproject["project"]["version"] == runtime_pyproject["project"]["version"]
|
||||
)
|
||||
assert sdk_pyproject["project"]["dependencies"] == [
|
||||
"pydantic>=2.12",
|
||||
"openai-codex-cli-bin==0.116.0a1",
|
||||
]
|
||||
|
||||
|
||||
def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
|
||||
script = _load_update_script_module()
|
||||
calls: list[str] = []
|
||||
@@ -363,18 +419,16 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
|
||||
[
|
||||
"stage-sdk",
|
||||
str(tmp_path / "sdk-stage"),
|
||||
"--runtime-version",
|
||||
"1.2.3",
|
||||
"--codex-version",
|
||||
"rust-v0.116.0-alpha.1",
|
||||
]
|
||||
)
|
||||
|
||||
def fake_generate_types() -> None:
|
||||
calls.append("generate_types")
|
||||
|
||||
def fake_stage_sdk_package(
|
||||
_staging_dir: Path, _sdk_version: str, _runtime_version: str
|
||||
) -> Path:
|
||||
calls.append("stage_sdk")
|
||||
def fake_stage_sdk_package(_staging_dir: Path, codex_version: str) -> Path:
|
||||
calls.append(f"stage_sdk:{codex_version}")
|
||||
return tmp_path / "sdk-stage"
|
||||
|
||||
def fake_stage_runtime_package(
|
||||
@@ -386,7 +440,7 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
|
||||
raise AssertionError("runtime staging should not run for stage-sdk")
|
||||
|
||||
def fake_current_sdk_version() -> str:
|
||||
return "0.2.0"
|
||||
return "0.116.0a1"
|
||||
|
||||
ops = script.CliOps(
|
||||
generate_types=fake_generate_types,
|
||||
@@ -397,7 +451,26 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
|
||||
|
||||
script.run_command(args, ops)
|
||||
|
||||
assert calls == ["generate_types", "stage_sdk"]
|
||||
assert calls == ["generate_types", "stage_sdk:0.116.0a1"]
|
||||
|
||||
|
||||
def test_stage_sdk_rejects_mismatched_legacy_versions(tmp_path: Path) -> None:
|
||||
script = _load_update_script_module()
|
||||
args = script.parse_args(
|
||||
[
|
||||
"stage-sdk",
|
||||
str(tmp_path / "sdk-stage"),
|
||||
"--codex-version",
|
||||
"0.116.0a1",
|
||||
"--runtime-version",
|
||||
"0.116.0a1",
|
||||
"--sdk-version",
|
||||
"0.115.0",
|
||||
]
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="versions must match"):
|
||||
script.run_command(args, script.default_cli_ops())
|
||||
|
||||
|
||||
def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> None:
|
||||
@@ -420,9 +493,7 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) ->
|
||||
def fake_generate_types() -> None:
|
||||
calls.append("generate_types")
|
||||
|
||||
def fake_stage_sdk_package(
|
||||
_staging_dir: Path, _sdk_version: str, _runtime_version: str
|
||||
) -> Path:
|
||||
def fake_stage_sdk_package(_staging_dir: Path, _codex_version: str) -> Path:
|
||||
raise AssertionError("sdk staging should not run for stage-runtime")
|
||||
|
||||
def fake_stage_runtime_package(
|
||||
@@ -435,7 +506,7 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) ->
|
||||
return tmp_path / "runtime-stage"
|
||||
|
||||
def fake_current_sdk_version() -> str:
|
||||
return "0.2.0"
|
||||
return "0.116.0a1"
|
||||
|
||||
ops = script.CliOps(
|
||||
generate_types=fake_generate_types,
|
||||
|
||||
@@ -2,8 +2,11 @@ from __future__ import annotations
|
||||
|
||||
import importlib.resources as resources
|
||||
import inspect
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import codex_app_server
|
||||
from codex_app_server import AppServerConfig, RunResult
|
||||
from codex_app_server.models import InitializeResponse
|
||||
from codex_app_server.api import AsyncCodex, AsyncThread, Codex, Thread
|
||||
@@ -37,6 +40,14 @@ def test_root_exports_run_result() -> None:
|
||||
assert RunResult.__name__ == "RunResult"
|
||||
|
||||
|
||||
def test_package_and_default_client_versions_follow_project_version() -> None:
|
||||
pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
||||
pyproject = tomllib.loads(pyproject_path.read_text())
|
||||
|
||||
assert codex_app_server.__version__ == pyproject["project"]["version"]
|
||||
assert AppServerConfig().client_version == codex_app_server.__version__
|
||||
|
||||
|
||||
def test_package_includes_py_typed_marker() -> None:
|
||||
marker = resources.files("codex_app_server").joinpath("py.typed")
|
||||
assert marker.is_file()
|
||||
@@ -54,6 +65,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"ephemeral",
|
||||
"model",
|
||||
"model_provider",
|
||||
"permission_profile",
|
||||
"personality",
|
||||
"sandbox",
|
||||
"service_name",
|
||||
@@ -70,6 +82,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"sort_direction",
|
||||
"sort_key",
|
||||
"source_kinds",
|
||||
"use_state_db_only",
|
||||
],
|
||||
Codex.thread_resume: [
|
||||
"approval_policy",
|
||||
@@ -78,8 +91,10 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"config",
|
||||
"cwd",
|
||||
"developer_instructions",
|
||||
"exclude_turns",
|
||||
"model",
|
||||
"model_provider",
|
||||
"permission_profile",
|
||||
"personality",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
@@ -92,8 +107,10 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"cwd",
|
||||
"developer_instructions",
|
||||
"ephemeral",
|
||||
"exclude_turns",
|
||||
"model",
|
||||
"model_provider",
|
||||
"permission_profile",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
],
|
||||
@@ -104,6 +121,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"effort",
|
||||
"model",
|
||||
"output_schema",
|
||||
"permission_profile",
|
||||
"personality",
|
||||
"sandbox_policy",
|
||||
"service_tier",
|
||||
@@ -116,6 +134,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"effort",
|
||||
"model",
|
||||
"output_schema",
|
||||
"permission_profile",
|
||||
"personality",
|
||||
"sandbox_policy",
|
||||
"service_tier",
|
||||
@@ -131,6 +150,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"ephemeral",
|
||||
"model",
|
||||
"model_provider",
|
||||
"permission_profile",
|
||||
"personality",
|
||||
"sandbox",
|
||||
"service_name",
|
||||
@@ -147,6 +167,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"sort_direction",
|
||||
"sort_key",
|
||||
"source_kinds",
|
||||
"use_state_db_only",
|
||||
],
|
||||
AsyncCodex.thread_resume: [
|
||||
"approval_policy",
|
||||
@@ -155,8 +176,10 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"config",
|
||||
"cwd",
|
||||
"developer_instructions",
|
||||
"exclude_turns",
|
||||
"model",
|
||||
"model_provider",
|
||||
"permission_profile",
|
||||
"personality",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
@@ -169,8 +192,10 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"cwd",
|
||||
"developer_instructions",
|
||||
"ephemeral",
|
||||
"exclude_turns",
|
||||
"model",
|
||||
"model_provider",
|
||||
"permission_profile",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
],
|
||||
@@ -181,6 +206,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"effort",
|
||||
"model",
|
||||
"output_schema",
|
||||
"permission_profile",
|
||||
"personality",
|
||||
"sandbox_policy",
|
||||
"service_tier",
|
||||
@@ -193,6 +219,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"effort",
|
||||
"model",
|
||||
"output_schema",
|
||||
"permission_profile",
|
||||
"personality",
|
||||
"sandbox_policy",
|
||||
"service_tier",
|
||||
|
||||
Generated
+25
-25
@@ -3,7 +3,7 @@ revision = 3
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-04-16T16:29:01.461661899Z"
|
||||
exclude-newer = "2026-04-20T18:19:27.620299Z"
|
||||
exclude-newer-span = "P7D"
|
||||
|
||||
[[package]]
|
||||
@@ -80,30 +80,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-app-server-sdk"
|
||||
version = "0.2.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "datamodel-code-generator" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = "==0.31.2" },
|
||||
{ name = "pydantic", specifier = ">=2.12" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@@ -301,6 +277,30 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai-codex-app-server-sdk"
|
||||
version = "0.116.0a1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "datamodel-code-generator" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = "==0.31.2" },
|
||||
{ name = "pydantic", specifier = ">=2.12" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.1"
|
||||
|
||||
Reference in New Issue
Block a user