[4/8] Define Python SDK public API surface (#21896)

## Why

The SDK package root should be the ergonomic public client API, not a
dump of every generated app-server schema type. Generated models still
need a supported import path, but callers should be able to tell which
names are high-level SDK entrypoints and which names are protocol value
models.

## What

- Define a curated root `__all__` for clients, handles, input helpers,
retry helpers, config, and public errors.
- Add a `types` module as the supported home for generated app-server
response, event, enum, and helper models.
- Update docs and examples to import protocol/value models from the type
module.
- Add tests that lock root exports, type-module exports, star-import
behavior, and example import hygiene.

## Stack

1. #21891 `[1/8]` Pin Python SDK runtime dependency
2. #21893 `[2/8]` Generate Python SDK types from pinned runtime
3. #21895 `[3/8]` Run Python SDK tests in CI
4. This PR `[4/8]` Define Python SDK public API surface
5. #21905 `[5/8]` Rename Python SDK package to `openai-codex`
6. #21910 `[6/8]` Add high-level Python SDK approval mode
7. #22014 `[7/8]` Add Python SDK app-server integration harness
8. #22021 `[8/8]` Add Python SDK Ruff formatting

## Verification

- Added public API signature tests for root exports, `types` exports,
and example imports.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-05-12 00:57:44 +03:00
committed by GitHub
parent 3e2936dd0e
commit b4bc02439f
16 changed files with 274 additions and 78 deletions

View File

@@ -7,12 +7,86 @@ 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
import codex_app_server.types as public_types
from codex_app_server import (
AppServerConfig,
AsyncCodex,
AsyncThread,
Codex,
RunResult,
Thread,
)
from codex_app_server.types import InitializeResponse
EXPECTED_ROOT_EXPORTS = [
"__version__",
"AppServerConfig",
"Codex",
"AsyncCodex",
"Thread",
"AsyncThread",
"TurnHandle",
"AsyncTurnHandle",
"RunResult",
"Input",
"InputItem",
"TextInput",
"ImageInput",
"LocalImageInput",
"SkillInput",
"MentionInput",
"retry_on_overload",
"AppServerError",
"TransportClosedError",
"JsonRpcError",
"AppServerRpcError",
"ParseError",
"InvalidRequestError",
"MethodNotFoundError",
"InvalidParamsError",
"InternalRpcError",
"ServerBusyError",
"RetryLimitExceededError",
"is_retryable_error",
]
EXPECTED_TYPES_EXPORTS = [
"ApprovalsReviewer",
"AskForApproval",
"InitializeResponse",
"JsonObject",
"ModelListResponse",
"Notification",
"Personality",
"PlanType",
"ReasoningEffort",
"ReasoningSummary",
"SandboxMode",
"SandboxPolicy",
"SortDirection",
"ThreadArchiveResponse",
"ThreadCompactStartResponse",
"ThreadItem",
"ThreadListCwdFilter",
"ThreadListResponse",
"ThreadReadResponse",
"ThreadSetNameResponse",
"ThreadSortKey",
"ThreadSource",
"ThreadSourceKind",
"ThreadStartSource",
"ThreadTokenUsage",
"ThreadTokenUsageUpdatedNotification",
"Turn",
"TurnCompletedNotification",
"TurnInterruptResponse",
"TurnStatus",
"TurnSteerResponse",
]
def _keyword_only_names(fn: object) -> list[str]:
"""Return only user-facing keyword-only parameter names for a public method."""
signature = inspect.signature(fn)
return [
param.name
@@ -22,6 +96,7 @@ def _keyword_only_names(fn: object) -> list[str]:
def _assert_no_any_annotations(fn: object) -> None:
"""Reject loose annotations on public wrapper methods."""
signature = inspect.signature(fn)
for param in signature.parameters.values():
if param.annotation is Any:
@@ -33,14 +108,17 @@ def _assert_no_any_annotations(fn: object) -> None:
def test_root_exports_app_server_config() -> None:
"""The root package should expose the process configuration object."""
assert AppServerConfig.__name__ == "AppServerConfig"
def test_root_exports_run_result() -> None:
"""The root package should expose the common-case run result wrapper."""
assert RunResult.__name__ == "RunResult"
def test_package_and_default_client_versions_follow_project_version() -> None:
"""The importable package version should stay aligned with pyproject metadata."""
pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml"
pyproject = tomllib.loads(pyproject_path.read_text())
@@ -49,10 +127,85 @@ def test_package_and_default_client_versions_follow_project_version() -> None:
def test_package_includes_py_typed_marker() -> None:
"""The wheel should advertise that inline type information is available."""
marker = resources.files("codex_app_server").joinpath("py.typed")
assert marker.is_file()
def test_package_root_exports_only_public_api() -> None:
"""The package root should expose the supported SDK surface, not internals."""
assert codex_app_server.__all__ == EXPECTED_ROOT_EXPORTS
assert {
name: hasattr(codex_app_server, name) for name in EXPECTED_ROOT_EXPORTS
} == {name: True for name in EXPECTED_ROOT_EXPORTS}
assert {
"AppServerClient": hasattr(codex_app_server, "AppServerClient"),
"AsyncAppServerClient": hasattr(codex_app_server, "AsyncAppServerClient"),
"InitializeResponse": hasattr(codex_app_server, "InitializeResponse"),
"ThreadStartParams": hasattr(codex_app_server, "ThreadStartParams"),
"TurnStartParams": hasattr(codex_app_server, "TurnStartParams"),
"TurnCompletedNotification": hasattr(
codex_app_server, "TurnCompletedNotification"
),
"TurnStatus": hasattr(codex_app_server, "TurnStatus"),
} == {
"AppServerClient": False,
"AsyncAppServerClient": False,
"InitializeResponse": False,
"ThreadStartParams": False,
"TurnStartParams": False,
"TurnCompletedNotification": False,
"TurnStatus": False,
}
def test_package_star_import_matches_public_api() -> None:
"""Star imports should follow the same explicit public API list."""
namespace: dict[str, object] = {}
exec("from codex_app_server import *", namespace)
exported = set(namespace) - {"__builtins__"}
assert exported == set(EXPECTED_ROOT_EXPORTS)
def test_types_module_exports_curated_public_types() -> None:
"""The public type module should be the supported place for app-server models."""
assert public_types.__all__ == EXPECTED_TYPES_EXPORTS
assert {name: hasattr(public_types, name) for name in EXPECTED_TYPES_EXPORTS} == {
name: True for name in EXPECTED_TYPES_EXPORTS
}
def test_types_star_import_matches_public_types() -> None:
"""Star imports from the type module should match its explicit export list."""
namespace: dict[str, object] = {}
exec("from codex_app_server.types import *", namespace)
exported = set(namespace) - {"__builtins__"}
assert exported == set(EXPECTED_TYPES_EXPORTS)
def test_examples_use_public_import_surfaces() -> None:
"""Examples should teach users the public root and type-module imports only."""
examples_root = Path(__file__).resolve().parents[1] / "examples"
private_import_markers = [
"codex_app_server.api",
"codex_app_server.client",
"codex_app_server.generated",
"codex_app_server.models",
"codex_app_server.retry",
]
offenders = {
str(path.relative_to(examples_root)): marker
for path in examples_root.rglob("*.py")
for marker in private_import_markers
if marker in path.read_text()
}
assert offenders == {}
def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"""Generated convenience methods should expose typed Pythonic keyword names."""
expected = {
@@ -228,6 +381,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
def test_lifecycle_methods_are_codex_scoped() -> None:
"""Lifecycle operations should hang off the client rather than thread objects."""
assert hasattr(Codex, "thread_resume")
assert hasattr(Codex, "thread_fork")
assert hasattr(Codex, "thread_archive")
@@ -258,6 +412,7 @@ def test_lifecycle_methods_are_codex_scoped() -> None:
def test_initialize_metadata_parses_user_agent_shape() -> None:
"""Initialize metadata should accept the legacy user-agent-only payload shape."""
payload = InitializeResponse.model_validate({"userAgent": "codex-cli/1.2.3"})
parsed = Codex._validate_initialize(payload)
assert parsed is payload
@@ -268,6 +423,7 @@ def test_initialize_metadata_parses_user_agent_shape() -> None:
def test_initialize_metadata_requires_non_empty_information() -> None:
"""Initialize metadata should fail when the runtime gives no identity signal."""
try:
Codex._validate_initialize(InitializeResponse.model_validate({}))
except RuntimeError as exc: