Add Bazel targets for on-demand schemas and Python SDK generation (#54096)

## What changed

- Add `//bazel/schema:app-server` to generate stable and experimental schemas from Rust sources without depending on committed precomputed bundles.
- Add `//sdk/python:generated` to generate Python SDK models, the notification registry, and API methods from the stable schema in scratch space, using dependencies from `uv.lock`.
- Add `generate-types --no-format` to skip formatters for build outputs while preserving formatting by default.

## Testing

Extend contract generation tests to regenerate in a temporary SDK copy, checking byte equality for formatted output and AST equality for unformatted output.

GitOrigin-RevId: 9ca7daa8ac0a33c4106a9e95c2f48bbded07df21
This commit is contained in:
Dmitriy Kovalenko
2026-10-14 20:59:31 +00:00
committed by copyberry
parent 12fd621f62
commit bc13ff239c
9 changed files with 298 additions and 27 deletions
+28 -18
View File
@@ -14,6 +14,7 @@ import tempfile
import types
import typing
from dataclasses import dataclass
from functools import partial
from pathlib import Path
from typing import Any, Callable, Sequence, get_args, get_origin
@@ -545,7 +546,7 @@ def _normalized_schema_bundle_text(schema_dir: Path) -> str:
return json.dumps(schema, indent=2, sort_keys=True) + "\n"
def generate_v2_all(schema_dir: Path) -> None:
def generate_v2_all(schema_dir: Path, *, format_output: bool = True) -> None:
"""Regenerate the Pydantic v2 protocol model module from app-server schemas."""
out_path = sdk_root() / "src" / "openai_codex" / "generated" / "v2_all.py"
out_dir = out_path.parent
@@ -556,6 +557,9 @@ def generate_v2_all(schema_dir: Path) -> None:
with tempfile.TemporaryDirectory() as td:
normalized_bundle = Path(td) / schema_bundle_path(schema_dir).name
normalized_bundle.write_text(_normalized_schema_bundle_text(schema_dir))
if not format_output:
# The pinned generator accepts an empty formatter list through TOML.
(Path(td) / "pyproject.toml").write_text("[tool.datamodel-codegen]\nformatters = []\n")
run_python_module(
"datamodel_code_generator",
[
@@ -582,18 +586,13 @@ def generate_v2_all(schema_dir: Path) -> None:
"--use-annotated",
"--use-union-operator",
"--disable-timestamp",
# Keep the generated file formatted deterministically so the
# checked-in artifact only changes when the schema does.
"--formatters",
"ruff-format",
],
cwd=sdk_root(),
]
+ (["--formatters", "ruff-format"] if format_output else []),
cwd=sdk_root() if format_output else Path(td),
)
_preserve_inline_image_class_names(out_path)
_require_nullable_field(out_path, "ChatgptAccount", r"email: str \| None")
_require_nullable_field(
out_path, "McpResourceReadTarget", r"link_id: Annotated\[\n(?: .*\n)+ \]"
)
_require_nullable_field(out_path, "McpResourceReadTarget", r"link_id: Annotated\[[\s\S]*?\]")
_preserve_open_enum(out_path, "CodexErrorInfoValue", allow_empty=True)
_preserve_reasoning_effort_enum(out_path)
_preserve_thread_source_enum(out_path)
@@ -1351,7 +1350,7 @@ def _render_thread_block(turn_fields: list[PublicFieldSpec], *, is_async: bool =
return "\n".join(lines)
def generate_public_api_flat_methods() -> None:
def generate_public_api_flat_methods(*, format_output: bool = True) -> None:
"""Regenerate the public convenience methods from generated protocol models."""
src_dir = sdk_root() / "src"
public_api_path = src_dir / "openai_codex" / "api.py"
@@ -1400,15 +1399,16 @@ def generate_public_api_flat_methods() -> None:
_render_thread_block(turn_start_fields, is_async=True),
)
public_api_path.write_text(source)
run_python_module("ruff", ["format", str(public_api_path)], cwd=sdk_root())
if format_output:
run_python_module("ruff", ["format", str(public_api_path)], cwd=sdk_root())
def generate_types_from_schema_dir(schema_dir: Path) -> None:
def generate_types_from_schema_dir(schema_dir: Path, *, format_output: bool = True) -> None:
"""Regenerate every SDK artifact derived from an existing schema directory."""
# v2_all is the authoritative generated surface.
generate_v2_all(schema_dir)
generate_v2_all(schema_dir, format_output=format_output)
generate_notification_registry(schema_dir)
generate_public_api_flat_methods()
generate_public_api_flat_methods(format_output=format_output)
def build_parser() -> argparse.ArgumentParser:
@@ -1423,6 +1423,12 @@ def build_parser() -> argparse.ArgumentParser:
type=Path,
help="App-server JSON schema directory (defaults to tool.codex.codegen.schema-dir)",
)
generate_types_parser.add_argument(
"--no-format",
dest="format_output",
action="store_false",
help="Generate build outputs without running formatters",
)
stage_sdk_parser = subparsers.add_parser(
"stage-sdk",
@@ -1483,9 +1489,13 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
return build_parser().parse_args(list(argv) if argv is not None else None)
def default_cli_ops() -> CliOps:
def default_cli_ops(*, format_output: bool = True) -> CliOps:
return CliOps(
generate_types=generate_types_from_schema_dir,
generate_types=(
generate_types_from_schema_dir
if format_output
else partial(generate_types_from_schema_dir, format_output=False)
),
stage_python_sdk_package=stage_python_sdk_package,
stage_python_runtime_package=stage_python_runtime_package,
)
@@ -1520,7 +1530,7 @@ def run_command(args: argparse.Namespace, ops: CliOps) -> None:
def main(argv: Sequence[str] | None = None, ops: CliOps | None = None) -> None:
args = parse_args(argv)
run_command(args, ops or default_cli_ops())
run_command(args, ops or default_cli_ops(format_output=getattr(args, "format_output", True)))
print("Done.")