mirror of
https://github.com/openai/codex.git
synced 2026-03-26 10:06:32 +03:00
Compare commits
8 Commits
dev/cc/rea
...
dev/shaqay
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a7db9e60b | ||
|
|
a961d299e0 | ||
|
|
89719adb45 | ||
|
|
1cab02f327 | ||
|
|
756556ddcb | ||
|
|
8942547f80 | ||
|
|
c270299b28 | ||
|
|
3d853de33f |
@@ -66,7 +66,7 @@ notebook bootstrap the pinned runtime package automatically.
|
||||
|
||||
```bash
|
||||
cd sdk/python
|
||||
python scripts/update_sdk_artifacts.py generate-types
|
||||
python scripts/update_sdk_artifacts.py generate-types-for-pinned-runtime
|
||||
python scripts/update_sdk_artifacts.py \
|
||||
stage-sdk \
|
||||
/tmp/codex-python-release/codex-app-server-sdk \
|
||||
@@ -80,7 +80,7 @@ python scripts/update_sdk_artifacts.py \
|
||||
|
||||
This supports the CI release flow:
|
||||
|
||||
- run `generate-types` before packaging
|
||||
- run `generate-types-for-pinned-runtime` before packaging
|
||||
- stage `codex-app-server-sdk` once with an exact `codex-cli-bin==...` dependency
|
||||
- stage `codex-cli-bin` on each supported platform runner with the same pinned runtime version
|
||||
- build and publish `codex-cli-bin` as platform wheels only; do not publish an sdist
|
||||
|
||||
@@ -65,7 +65,7 @@ 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 generate-types-for-pinned-runtime
|
||||
python scripts/update_sdk_artifacts.py \
|
||||
stage-sdk \
|
||||
/tmp/codex-python-release/codex-app-server-sdk \
|
||||
|
||||
@@ -30,7 +30,12 @@ Repository = "https://github.com/openai/codex"
|
||||
Issues = "https://github.com/openai/codex/issues"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=8.0", "datamodel-code-generator==0.31.2", "ruff>=0.11"]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
"datamodel-code-generator==0.31.2",
|
||||
"ruff>=0.11",
|
||||
"tomli>=2.0; python_version < '3.11'",
|
||||
]
|
||||
|
||||
[tool.hatch.build]
|
||||
exclude = [
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import importlib.util
|
||||
import json
|
||||
import platform
|
||||
import re
|
||||
@@ -65,6 +66,85 @@ def run_python_module(module: str, args: list[str], cwd: Path) -> None:
|
||||
run([sys.executable, "-m", module, *args], cwd)
|
||||
|
||||
|
||||
def run_capture(cmd: list[str], cwd: Path) -> str:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=str(cwd),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"Command failed ({result.returncode}): {' '.join(cmd)}\n"
|
||||
f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def runtime_setup_path() -> Path:
|
||||
return sdk_root() / "_runtime_setup.py"
|
||||
|
||||
|
||||
def pinned_runtime_version() -> str:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"_runtime_setup", runtime_setup_path()
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Failed to load runtime setup module: {runtime_setup_path()}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module.pinned_runtime_version() # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def runtime_git_tag(version: str) -> str:
|
||||
return f"rust-v{version}"
|
||||
|
||||
|
||||
def pinned_runtime_git_tag() -> str:
|
||||
return runtime_git_tag(pinned_runtime_version())
|
||||
|
||||
|
||||
def ensure_git_tag_available(git_tag: str) -> None:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--verify", git_tag],
|
||||
cwd=str(repo_root()),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return
|
||||
run(["git", "fetch", "origin", "tag", git_tag, "--depth=1"], repo_root())
|
||||
|
||||
|
||||
def read_git_tag_file(git_tag: str, repo_path: str) -> str:
|
||||
return run_capture(["git", "show", f"{git_tag}:{repo_path}"], repo_root())
|
||||
|
||||
|
||||
def materialize_schema_files_from_git_tag(git_tag: str, out_dir: Path) -> tuple[Path, Path]:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
schema_bundle = out_dir / "codex_app_server_protocol.v2.schemas.json"
|
||||
schema_bundle.write_text(
|
||||
read_git_tag_file(
|
||||
git_tag,
|
||||
"codex-rs/app-server-protocol/schema/json/"
|
||||
"codex_app_server_protocol.v2.schemas.json",
|
||||
)
|
||||
)
|
||||
|
||||
server_notification = out_dir / "ServerNotification.json"
|
||||
server_notification.write_text(
|
||||
read_git_tag_file(
|
||||
git_tag,
|
||||
"codex-rs/app-server-protocol/schema/json/ServerNotification.json",
|
||||
)
|
||||
)
|
||||
|
||||
return schema_bundle, server_notification
|
||||
|
||||
|
||||
def current_sdk_version() -> str:
|
||||
match = re.search(
|
||||
r'^version = "([^"]+)"$',
|
||||
@@ -396,8 +476,9 @@ def _annotate_schema(value: Any, base: str | None = None) -> None:
|
||||
_annotate_schema(child, base)
|
||||
|
||||
|
||||
def _normalized_schema_bundle_text() -> str:
|
||||
schema = json.loads(schema_bundle_path().read_text())
|
||||
def _normalized_schema_bundle_text(schema_bundle: Path | None = None) -> str:
|
||||
bundle = schema_bundle or schema_bundle_path()
|
||||
schema = json.loads(bundle.read_text())
|
||||
definitions = schema.get("definitions", {})
|
||||
if isinstance(definitions, dict):
|
||||
for definition in definitions.values():
|
||||
@@ -409,16 +490,17 @@ def _normalized_schema_bundle_text() -> str:
|
||||
return json.dumps(schema, indent=2, sort_keys=True) + "\n"
|
||||
|
||||
|
||||
def generate_v2_all() -> None:
|
||||
def generate_v2_all(schema_bundle: Path | None = None) -> None:
|
||||
out_path = sdk_root() / "src" / "codex_app_server" / "generated" / "v2_all.py"
|
||||
out_dir = out_path.parent
|
||||
old_package_dir = out_dir / "v2_all"
|
||||
if old_package_dir.exists():
|
||||
shutil.rmtree(old_package_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
bundle = schema_bundle or schema_bundle_path()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
normalized_bundle = Path(td) / schema_bundle_path().name
|
||||
normalized_bundle.write_text(_normalized_schema_bundle_text())
|
||||
normalized_bundle = Path(td) / bundle.name
|
||||
normalized_bundle.write_text(_normalized_schema_bundle_text(bundle))
|
||||
run_python_module(
|
||||
"datamodel_code_generator",
|
||||
[
|
||||
@@ -455,9 +537,14 @@ def generate_v2_all() -> None:
|
||||
_normalize_generated_timestamps(out_path)
|
||||
|
||||
|
||||
def _notification_specs() -> list[tuple[str, str]]:
|
||||
def _notification_specs(
|
||||
server_notification_schema: Path | None = None,
|
||||
) -> list[tuple[str, str]]:
|
||||
server_notification_path = server_notification_schema or (
|
||||
schema_root_dir() / "ServerNotification.json"
|
||||
)
|
||||
server_notifications = json.loads(
|
||||
(schema_root_dir() / "ServerNotification.json").read_text()
|
||||
server_notification_path.read_text()
|
||||
)
|
||||
one_of = server_notifications.get("oneOf", [])
|
||||
generated_source = (
|
||||
@@ -494,7 +581,9 @@ def _notification_specs() -> list[tuple[str, str]]:
|
||||
return specs
|
||||
|
||||
|
||||
def generate_notification_registry() -> None:
|
||||
def generate_notification_registry(
|
||||
server_notification_schema: Path | None = None,
|
||||
) -> None:
|
||||
out = (
|
||||
sdk_root()
|
||||
/ "src"
|
||||
@@ -502,7 +591,7 @@ def generate_notification_registry() -> None:
|
||||
/ "generated"
|
||||
/ "notification_registry.py"
|
||||
)
|
||||
specs = _notification_specs()
|
||||
specs = _notification_specs(server_notification_schema)
|
||||
class_names = sorted({class_name for _, class_name in specs})
|
||||
|
||||
lines = [
|
||||
@@ -558,6 +647,8 @@ class PublicFieldSpec:
|
||||
@dataclass(frozen=True)
|
||||
class CliOps:
|
||||
generate_types: Callable[[], None]
|
||||
generate_types_for_pinned_runtime: Callable[[], None]
|
||||
generate_types_for_runtime_tag: Callable[[str], None]
|
||||
stage_python_sdk_package: Callable[[Path, str, str], Path]
|
||||
stage_python_runtime_package: Callable[[Path, str, Path], Path]
|
||||
current_sdk_version: Callable[[], str]
|
||||
@@ -867,9 +958,9 @@ def generate_public_api_flat_methods() -> None:
|
||||
exclude={"thread_id", "input"},
|
||||
)
|
||||
|
||||
source = public_api_path.read_text()
|
||||
original_source = public_api_path.read_text()
|
||||
source = _replace_generated_block(
|
||||
source,
|
||||
original_source,
|
||||
"Codex.flat_methods",
|
||||
_render_codex_block(
|
||||
thread_start_fields,
|
||||
@@ -898,16 +989,38 @@ def generate_public_api_flat_methods() -> None:
|
||||
"AsyncThread.flat_methods",
|
||||
_render_async_thread_block(turn_start_fields),
|
||||
)
|
||||
if source == original_source:
|
||||
return
|
||||
public_api_path.write_text(source)
|
||||
|
||||
|
||||
def generate_types() -> None:
|
||||
def generate_types(
|
||||
schema_bundle: Path | None = None,
|
||||
server_notification_schema: Path | None = None,
|
||||
) -> None:
|
||||
# v2_all is the authoritative generated surface.
|
||||
generate_v2_all()
|
||||
generate_notification_registry()
|
||||
generate_v2_all(schema_bundle)
|
||||
generate_notification_registry(server_notification_schema)
|
||||
generate_public_api_flat_methods()
|
||||
|
||||
|
||||
def generate_types_for_runtime_tag(runtime_tag: str) -> None:
|
||||
ensure_git_tag_available(runtime_tag)
|
||||
with tempfile.TemporaryDirectory(prefix="codex-python-pinned-schema-") as temp_root:
|
||||
schema_bundle, server_notification_schema = materialize_schema_files_from_git_tag(
|
||||
runtime_tag,
|
||||
Path(temp_root),
|
||||
)
|
||||
generate_types(
|
||||
schema_bundle=schema_bundle,
|
||||
server_notification_schema=server_notification_schema,
|
||||
)
|
||||
|
||||
|
||||
def generate_types_for_pinned_runtime() -> None:
|
||||
generate_types_for_runtime_tag(pinned_runtime_git_tag())
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Single SDK maintenance entrypoint")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
@@ -915,6 +1028,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
subparsers.add_parser(
|
||||
"generate-types", help="Regenerate Python protocol-derived types"
|
||||
)
|
||||
subparsers.add_parser(
|
||||
"generate-types-for-pinned-runtime",
|
||||
help="Regenerate Python protocol-derived types from the pinned runtime version",
|
||||
)
|
||||
|
||||
stage_sdk_parser = subparsers.add_parser(
|
||||
"stage-sdk",
|
||||
@@ -964,6 +1081,8 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||||
def default_cli_ops() -> CliOps:
|
||||
return CliOps(
|
||||
generate_types=generate_types,
|
||||
generate_types_for_pinned_runtime=generate_types_for_pinned_runtime,
|
||||
generate_types_for_runtime_tag=generate_types_for_runtime_tag,
|
||||
stage_python_sdk_package=stage_python_sdk_package,
|
||||
stage_python_runtime_package=stage_python_runtime_package,
|
||||
current_sdk_version=current_sdk_version,
|
||||
@@ -973,8 +1092,10 @@ def default_cli_ops() -> CliOps:
|
||||
def run_command(args: argparse.Namespace, ops: CliOps) -> None:
|
||||
if args.command == "generate-types":
|
||||
ops.generate_types()
|
||||
elif args.command == "generate-types-for-pinned-runtime":
|
||||
ops.generate_types_for_pinned_runtime()
|
||||
elif args.command == "stage-sdk":
|
||||
ops.generate_types()
|
||||
ops.generate_types_for_runtime_tag(runtime_git_tag(args.runtime_version))
|
||||
ops.stage_python_sdk_package(
|
||||
args.staging_dir,
|
||||
args.sdk_version or ops.current_sdk_version(),
|
||||
|
||||
@@ -52,6 +52,23 @@ from ._run import (
|
||||
_collect_run_result,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AsyncCodex",
|
||||
"AsyncThread",
|
||||
"AsyncTurnHandle",
|
||||
"Codex",
|
||||
"ImageInput",
|
||||
"Input",
|
||||
"InputItem",
|
||||
"LocalImageInput",
|
||||
"MentionInput",
|
||||
"RunResult",
|
||||
"SkillInput",
|
||||
"TextInput",
|
||||
"Thread",
|
||||
"TurnHandle",
|
||||
]
|
||||
|
||||
|
||||
def _split_user_agent(user_agent: str) -> tuple[str | None, str | None]:
|
||||
raw = user_agent.strip()
|
||||
|
||||
@@ -1133,6 +1133,13 @@ class GuardianRiskLevel(Enum):
|
||||
high = "high"
|
||||
|
||||
|
||||
class HazelnutScope(Enum):
|
||||
example = "example"
|
||||
workspace_shared = "workspace-shared"
|
||||
all_shared = "all-shared"
|
||||
personal = "personal"
|
||||
|
||||
|
||||
class HookEventName(Enum):
|
||||
session_start = "sessionStart"
|
||||
stop = "stop"
|
||||
@@ -1378,13 +1385,6 @@ class LogoutAccountResponse(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class MarketplaceInterface(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
display_name: Annotated[str | None, Field(alias="displayName")] = None
|
||||
|
||||
|
||||
class McpAuthStatus(Enum):
|
||||
unsupported = "unsupported"
|
||||
not_logged_in = "notLoggedIn"
|
||||
@@ -1633,13 +1633,6 @@ class PluginInstallParams(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
force_remote_sync: Annotated[
|
||||
bool | None,
|
||||
Field(
|
||||
alias="forceRemoteSync",
|
||||
description="When true, apply the remote plugin change before the local install flow.",
|
||||
),
|
||||
] = None
|
||||
marketplace_path: Annotated[AbsolutePathBuf, Field(alias="marketplacePath")]
|
||||
plugin_name: Annotated[str, Field(alias="pluginName")]
|
||||
|
||||
@@ -1744,13 +1737,6 @@ class PluginUninstallParams(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
force_remote_sync: Annotated[
|
||||
bool | None,
|
||||
Field(
|
||||
alias="forceRemoteSync",
|
||||
description="When true, apply the remote plugin change before the local uninstall flow.",
|
||||
),
|
||||
] = None
|
||||
plugin_id: Annotated[str, Field(alias="pluginId")]
|
||||
|
||||
|
||||
@@ -1761,6 +1747,13 @@ class PluginUninstallResponse(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class ProductSurface(Enum):
|
||||
chatgpt = "chatgpt"
|
||||
codex = "codex"
|
||||
api = "api"
|
||||
atlas = "atlas"
|
||||
|
||||
|
||||
class RateLimitWindow(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -1913,6 +1906,15 @@ class ReasoningTextDeltaNotification(BaseModel):
|
||||
turn_id: Annotated[str, Field(alias="turnId")]
|
||||
|
||||
|
||||
class RemoteSkillSummary(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
description: str
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
class RequestId(RootModel[str | int]):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -1972,6 +1974,7 @@ class ReasoningResponseItem(BaseModel):
|
||||
)
|
||||
content: list[ReasoningItemContent] | None = None
|
||||
encrypted_content: str | None = None
|
||||
id: str
|
||||
summary: list[ReasoningItemReasoningSummary]
|
||||
type: Annotated[Literal["reasoning"], Field(title="ReasoningResponseItemType")]
|
||||
|
||||
@@ -2596,6 +2599,41 @@ class SkillsListParams(BaseModel):
|
||||
] = None
|
||||
|
||||
|
||||
class SkillsRemoteReadParams(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
enabled: bool | None = False
|
||||
hazelnut_scope: Annotated[HazelnutScope | None, Field(alias="hazelnutScope")] = (
|
||||
"example"
|
||||
)
|
||||
product_surface: Annotated[ProductSurface | None, Field(alias="productSurface")] = (
|
||||
"codex"
|
||||
)
|
||||
|
||||
|
||||
class SkillsRemoteReadResponse(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
data: list[RemoteSkillSummary]
|
||||
|
||||
|
||||
class SkillsRemoteWriteParams(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
hazelnut_id: Annotated[str, Field(alias="hazelnutId")]
|
||||
|
||||
|
||||
class SkillsRemoteWriteResponse(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
id: str
|
||||
path: str
|
||||
|
||||
|
||||
class SubAgentSourceValue(Enum):
|
||||
review = "review"
|
||||
compact = "compact"
|
||||
@@ -3012,7 +3050,6 @@ class ThreadRealtimeAudioChunk(BaseModel):
|
||||
populate_by_name=True,
|
||||
)
|
||||
data: str
|
||||
item_id: Annotated[str | None, Field(alias="itemId")] = None
|
||||
num_channels: Annotated[int, Field(alias="numChannels", ge=0)]
|
||||
sample_rate: Annotated[int, Field(alias="sampleRate", ge=0)]
|
||||
samples_per_channel: Annotated[
|
||||
@@ -3761,6 +3798,29 @@ class PluginReadRequest(BaseModel):
|
||||
params: PluginReadParams
|
||||
|
||||
|
||||
class SkillsRemoteListRequest(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
id: RequestId
|
||||
method: Annotated[
|
||||
Literal["skills/remote/list"], Field(title="Skills/remote/listRequestMethod")
|
||||
]
|
||||
params: SkillsRemoteReadParams
|
||||
|
||||
|
||||
class SkillsRemoteExportRequest(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
id: RequestId
|
||||
method: Annotated[
|
||||
Literal["skills/remote/export"],
|
||||
Field(title="Skills/remote/exportRequestMethod"),
|
||||
]
|
||||
params: SkillsRemoteWriteParams
|
||||
|
||||
|
||||
class AppListRequest(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -4619,7 +4679,6 @@ class PluginMarketplaceEntry(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
interface: MarketplaceInterface | None = None
|
||||
name: str
|
||||
path: AbsolutePathBuf
|
||||
plugins: list[PluginSummary]
|
||||
@@ -5530,6 +5589,14 @@ class FunctionCallOutputBody(RootModel[str | list[FunctionCallOutputContentItem]
|
||||
root: str | list[FunctionCallOutputContentItem]
|
||||
|
||||
|
||||
class FunctionCallOutputPayload(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
body: FunctionCallOutputBody
|
||||
success: bool | None = None
|
||||
|
||||
|
||||
class GetAccountRateLimitsResponse(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -5627,7 +5694,7 @@ class FunctionCallOutputResponseItem(BaseModel):
|
||||
populate_by_name=True,
|
||||
)
|
||||
call_id: str
|
||||
output: FunctionCallOutputBody
|
||||
output: FunctionCallOutputPayload
|
||||
type: Annotated[
|
||||
Literal["function_call_output"],
|
||||
Field(title="FunctionCallOutputResponseItemType"),
|
||||
@@ -5639,7 +5706,7 @@ class CustomToolCallOutputResponseItem(BaseModel):
|
||||
populate_by_name=True,
|
||||
)
|
||||
call_id: str
|
||||
output: FunctionCallOutputBody
|
||||
output: FunctionCallOutputPayload
|
||||
type: Annotated[
|
||||
Literal["custom_tool_call_output"],
|
||||
Field(title="CustomToolCallOutputResponseItemType"),
|
||||
@@ -6072,6 +6139,8 @@ class ClientRequest(
|
||||
| SkillsListRequest
|
||||
| PluginListRequest
|
||||
| PluginReadRequest
|
||||
| SkillsRemoteListRequest
|
||||
| SkillsRemoteExportRequest
|
||||
| AppListRequest
|
||||
| FsReadFileRequest
|
||||
| FsWriteFileRequest
|
||||
@@ -6133,6 +6202,8 @@ class ClientRequest(
|
||||
| SkillsListRequest
|
||||
| PluginListRequest
|
||||
| PluginReadRequest
|
||||
| SkillsRemoteListRequest
|
||||
| SkillsRemoteExportRequest
|
||||
| AppListRequest
|
||||
| FsReadFileRequest
|
||||
| FsWriteFileRequest
|
||||
|
||||
@@ -5,12 +5,16 @@ import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tomllib
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError: # pragma: no cover - Python 3.10 fallback.
|
||||
import tomli as tomllib
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@@ -168,6 +172,37 @@ def test_examples_readme_matches_pinned_runtime_version() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_pinned_runtime_git_tag_matches_runtime_setup_pin() -> None:
|
||||
script = _load_update_script_module()
|
||||
runtime_setup = _load_runtime_setup_module()
|
||||
|
||||
assert script.pinned_runtime_git_tag() == (
|
||||
f"rust-v{runtime_setup.pinned_runtime_version()}"
|
||||
)
|
||||
|
||||
|
||||
def test_parser_supports_generate_types_for_pinned_runtime() -> None:
|
||||
script = _load_update_script_module()
|
||||
|
||||
args = script.parse_args(["generate-types-for-pinned-runtime"])
|
||||
|
||||
assert args.command == "generate-types-for-pinned-runtime"
|
||||
assert not hasattr(args, "git_ref")
|
||||
|
||||
|
||||
def test_parser_rejects_git_ref_override_for_pinned_runtime_generation() -> None:
|
||||
script = _load_update_script_module()
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
script.parse_args(
|
||||
[
|
||||
"generate-types-for-pinned-runtime",
|
||||
"--git-ref",
|
||||
"rust-v1.2.3",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_release_metadata_retries_without_invalid_auth(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
runtime_setup = _load_runtime_setup_module()
|
||||
authorizations: list[str | None] = []
|
||||
@@ -193,9 +228,7 @@ def test_release_metadata_retries_without_invalid_auth(monkeypatch: pytest.Monke
|
||||
|
||||
|
||||
def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() -> None:
|
||||
pyproject = tomllib.loads(
|
||||
(ROOT.parent / "python-runtime" / "pyproject.toml").read_text()
|
||||
)
|
||||
pyproject = tomllib.loads((ROOT.parent / "python-runtime" / "pyproject.toml").read_text())
|
||||
hook_source = (ROOT.parent / "python-runtime" / "hatch_build.py").read_text()
|
||||
hook_tree = ast.parse(hook_source)
|
||||
initialize_fn = next(
|
||||
@@ -320,7 +353,13 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
def fake_generate_types() -> None:
|
||||
calls.append("generate_types")
|
||||
raise AssertionError("stage-sdk should use pinned-runtime generation")
|
||||
|
||||
def fake_generate_types_for_pinned_runtime() -> None:
|
||||
raise AssertionError("stage-sdk should use explicit runtime-tag generation")
|
||||
|
||||
def fake_generate_types_for_runtime_tag(runtime_tag: str) -> None:
|
||||
calls.append(f"generate_types_for_runtime_tag:{runtime_tag}")
|
||||
|
||||
def fake_stage_sdk_package(
|
||||
_staging_dir: Path, _sdk_version: str, _runtime_version: str
|
||||
@@ -338,6 +377,8 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
|
||||
|
||||
ops = script.CliOps(
|
||||
generate_types=fake_generate_types,
|
||||
generate_types_for_pinned_runtime=fake_generate_types_for_pinned_runtime,
|
||||
generate_types_for_runtime_tag=fake_generate_types_for_runtime_tag,
|
||||
stage_python_sdk_package=fake_stage_sdk_package,
|
||||
stage_python_runtime_package=fake_stage_runtime_package,
|
||||
current_sdk_version=fake_current_sdk_version,
|
||||
@@ -345,7 +386,7 @@ 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_for_runtime_tag:rust-v1.2.3", "stage_sdk"]
|
||||
|
||||
|
||||
def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> None:
|
||||
@@ -366,6 +407,12 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) ->
|
||||
def fake_generate_types() -> None:
|
||||
calls.append("generate_types")
|
||||
|
||||
def fake_generate_types_for_pinned_runtime() -> None:
|
||||
calls.append("generate_types_for_pinned_runtime")
|
||||
|
||||
def fake_generate_types_for_runtime_tag(_runtime_tag: str) -> None:
|
||||
calls.append("generate_types_for_runtime_tag")
|
||||
|
||||
def fake_stage_sdk_package(
|
||||
_staging_dir: Path, _sdk_version: str, _runtime_version: str
|
||||
) -> Path:
|
||||
@@ -382,6 +429,8 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) ->
|
||||
|
||||
ops = script.CliOps(
|
||||
generate_types=fake_generate_types,
|
||||
generate_types_for_pinned_runtime=fake_generate_types_for_pinned_runtime,
|
||||
generate_types_for_runtime_tag=fake_generate_types_for_runtime_tag,
|
||||
stage_python_sdk_package=fake_stage_sdk_package,
|
||||
stage_python_runtime_package=fake_stage_runtime_package,
|
||||
current_sdk_version=fake_current_sdk_version,
|
||||
|
||||
@@ -33,20 +33,24 @@ def _snapshot_targets(root: Path) -> dict[str, dict[str, bytes] | bytes | None]:
|
||||
}
|
||||
|
||||
|
||||
def test_generated_files_are_up_to_date():
|
||||
def test_pinned_runtime_generated_files_are_up_to_date() -> None:
|
||||
before = _snapshot_targets(ROOT)
|
||||
|
||||
# Regenerate contract artifacts via single maintenance entrypoint.
|
||||
# Regenerate checked-in artifacts from the pinned runtime schema.
|
||||
env = os.environ.copy()
|
||||
python_bin = str(Path(sys.executable).parent)
|
||||
env["PATH"] = f"{python_bin}{os.pathsep}{env.get('PATH', '')}"
|
||||
|
||||
subprocess.run(
|
||||
[sys.executable, "scripts/update_sdk_artifacts.py", "generate-types"],
|
||||
[
|
||||
sys.executable,
|
||||
"scripts/update_sdk_artifacts.py",
|
||||
"generate-types-for-pinned-runtime",
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
after = _snapshot_targets(ROOT)
|
||||
assert before == after, "Generated files drifted after regeneration"
|
||||
assert before == after, "Generated files drifted after pinned-runtime regeneration"
|
||||
|
||||
Reference in New Issue
Block a user