scripts.gen_cxp

Reference › scripts › gen_cxp

scripts/gen_cxp.py regenerates the CXP protobuf message stubs cxp_pb2.py and cxp_pb2.pyi from cxp.proto. A human runs it after changing the proto. The CXP codec imports cxp_pb2 at runtime, so the output is a runtime input; see the cxp module.

It takes no arguments. main requires uv and a GIL-enabled CPython 3.14 (the PROTOC_PYTHON path, resolved by _default_protoc_python), because grpcio-tools has no free-threaded wheel; it exits 1 with a message when either is missing. _protoc_command builds the ephemeral-environment protoc invocation, which runs in a temporary directory. The results are then published into the proto directory with dump_text_atomic from orchestrata_ultimate.core.atomic_json, so a concurrent importer never reads a half-written module. Finally it runs ruff check --fix and black over the proto directory, because protoc and mypy-protobuf emit legacy Optional and Union annotations and unsorted imports that the repository's lint gate rejects. It prints a one-line confirmation on success.

At a glance

Source scripts/gen_cxp.py
Lines of code 169
Classes 0
Functions 3
Imports (internal) orchestrata_ultimate.core.atomic_json
Imported by none
Tests that import it none

Class diagram

No classes; see Functions.

Dependency diagram

Dependency diagram scripts.gen_cxp

API

Functions

Function Signature Summary
_default_protoc_python _default_protoc_python() -> str Resolve a GIL-enabled CPython 3.14 to host protoc/grpcio-tools.
_protoc_command _protoc_command(out_dir: str) -> list[str] Build the ephemeral-environment protoc invocation.
main main() -> int

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/scripts/gen_cxp.py

#!/usr/bin/env python3
"""Regenerate the CXP protobuf message stubs from cxp.proto.

Emits ``cxp_pb2.py`` + ``cxp_pb2.pyi`` only. The gRPC service stubs
(``cxp_pb2_grpc.py``) are no longer generated: the ``grpc.aio`` CXP transport
was retired on 2026-08-07 (SPEC-023 -- its EventEngine threads re-enable the
GIL) in favour of the cxpt direct transport, which speaks the same protobuf
messages over its own framing. protobuf is a codec and was never coupled to
the RPC runtime, so the messages stay and the service stubs go. See
``plans/grpc-retirement-design.md``.

**protoc runs in an ephemeral, GIL-enabled interpreter.** ``grpcio-tools`` is
deliberately NOT a project dependency:

  - it ships no ``cp314t`` wheel (checked 2026-08-07: 1.83.0 publishes
    ``cp314`` only), so depending on it meant vendoring a hand-built
    free-threaded wheel under ``vendor/wheels/`` -- which is gitignored, so a
    fresh clone (i.e. CI) could not resolve it and ``uv sync --dev`` failed;
  - it never needs to be free-threaded in the first place. protoc is a code
    generator invoked as a SUBPROCESS; it emits text and exits. Nothing about
    the GIL in that child process can affect this project's runtime.

So it is fetched on demand into a throwaway environment built on the system
GIL-enabled 3.14 interpreter. uv caches the wheels, so repeat runs are fast
and offline.

The grpcio-tools version is PINNED because protoc's output carries a gencode
version that the installed ``protobuf`` runtime validates: a runtime older
than the gencode is a hard ``VersionError`` at import. 1.80.0 emits gencode
6.31.1, which the pinned ``protobuf>=4.25`` runtime accepts. (The system
``/usr/bin/protoc`` 35.1 emits gencode 7.35.1 and is therefore unusable here
-- verified 2026-08-07.) Bumping this pin means checking the generated
``cxp_pb2.py`` still imports.

Run from the repo root:

    uv run python scripts/gen_cxp.py
"""

from __future__ import annotations

import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

PROTO_DIR = Path("src/orchestrata_ultimate/cxp/proto")
PROTO = PROTO_DIR / "cxp.proto"

# See the module docstring: pinned so the emitted gencode version stays
# compatible with the `protobuf` runtime this project pins.
GRPCIO_TOOLS_PIN = "grpcio-tools==1.80.0"


# A GIL-enabled interpreter for the throwaway protoc env. protoc is a
# subprocess code generator, so the GIL is irrelevant to it -- and the
# free-threaded build has no grpcio-tools wheel.
def _default_protoc_python() -> str:
    """Resolve a GIL-enabled CPython 3.14 to host protoc/grpcio-tools.

    Prefers the conventional system interpreter (present on the project's
    reference Debian hosts, where this is a no-op). Falls back to a
    uv-managed, exactly-pinned build on hosts that lack it (verified
    2026-09-23: a dev host here has no ``/usr/bin/python3.14`` and never
    will via its native package manager -- it is not Debian). A bare
    ``--python 3.14`` request is not a substitute: uv resolves that
    ambiguous version against whichever build is already active for this
    project (free-threaded, per ``requires-python``) rather than the
    GIL-enabled one grpcio-tools needs -- verified empirically, not assumed
    -- so the fallback pins the exact patch and asks uv for it by full
    version instead.
    """
    system_python = Path("/usr/bin/python3.14")
    if system_python.exists() or shutil.which("uv") is None:
        return str(system_python)
    pin = "3.14.3"
    try:
        subprocess.run(
            ["uv", "python", "install", pin],
            capture_output=True,
            text=True,
            check=False,
        )
        found = subprocess.run(
            ["uv", "python", "find", pin], capture_output=True, text=True, check=False
        )
    except OSError:
        return str(system_python)
    candidate = found.stdout.strip()
    if found.returncode == 0 and candidate and Path(candidate).exists():
        return candidate
    return str(system_python)


PROTOC_PYTHON = _default_protoc_python()


def _protoc_command(out_dir: str) -> list[str]:
    """Build the ephemeral-environment protoc invocation."""
    return [
        "uv",
        "run",
        "--isolated",
        "--no-project",
        "--python",
        PROTOC_PYTHON,
        "--with",
        GRPCIO_TOOLS_PIN,
        "python",
        "-m",
        "grpc_tools.protoc",
        "-I",
        str(PROTO_DIR),
        f"--python_out={out_dir}",
        f"--pyi_out={out_dir}",
        str(PROTO),
    ]


def main() -> int:
    if shutil.which("uv") is None:
        print(
            "uv is required to build the ephemeral protoc environment; "
            "see https://docs.astral.sh/uv/",
            file=sys.stderr,
        )
        return 1
    if not Path(PROTOC_PYTHON).exists():
        print(
            f"{PROTOC_PYTHON} not found -- a GIL-enabled CPython 3.14 is "
            "needed to host grpcio-tools (it ships no cp314t wheel). Install "
            "the system python3.14 package or edit PROTOC_PYTHON.",
            file=sys.stderr,
        )
        return 1

    with tempfile.TemporaryDirectory() as td:
        rc = subprocess.run(_protoc_command(td), capture_output=True, text=True)
        if rc.returncode != 0:
            print(rc.stderr, file=sys.stderr)
            return 1
        tmp = Path(td)
        # Atomic, and here it matters more than for a data artifact: these two
        # are runtime_input — the CXP codec IMPORTS cxp_pb2. Truncating the
        # module in place keeps its inode, so a concurrent importer or a
        # `git update-index` hashing it can read a half-written module or die
        # with SIGBUS. Publishing by replace gives them a new inode and leaves
        # any existing reader on the intact previous file.
        from orchestrata_ultimate.core.atomic_json import dump_text_atomic

        dump_text_atomic(PROTO_DIR / "cxp_pb2.py", (tmp / "cxp_pb2.py").read_text())
        dump_text_atomic(PROTO_DIR / "cxp_pb2.pyi", (tmp / "cxp_pb2.pyi").read_text())

    # protoc/mypy-protobuf emit legacy `Optional[...]`/`Union[...]` annotations
    # and unsorted imports in the .pyi stub, which trip the repo's ruff gate
    # (UP045/UP007/I001). Auto-fix them so regenerated files stay lint-clean
    # rather than reintroducing 50+ pre-existing errors on every regen.
    subprocess.run(
        ["uv", "run", "ruff", "check", "--fix", "-q", str(PROTO_DIR)],
        check=False,
    )
    subprocess.run(["uv", "run", "black", "-q", str(PROTO_DIR)], check=False)
    print("regenerated cxp_pb2.py, cxp_pb2.pyi")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())