Source code for edri.api.grpc.export
"""Render edri.api.grpc.schema's generated FileDescriptorProto to .proto3
source text, for consumers who want a static `.proto` file to run through
their own language's codegen instead of pointing it at the reflection
service. Pure text generation over the FileDescriptorProto's own fields -
no DescriptorPool/message_factory/live message classes involved (unlike
edri/api/grpc/runtime.py), so this direction doesn't touch anything beyond
the plain (proto2-defined) descriptor_pb2 shape schema.py already builds.
Usage: ``python -m edri.api.grpc.export [output_path]`` - writes to
`output_path` if given, otherwise to stdout.
"""
from __future__ import annotations
import sys
from google.protobuf import descriptor_pb2
from edri.api.grpc.schema import build_schema
__all__ = ["render_proto"]
_FD = descriptor_pb2.FieldDescriptorProto
_SCALAR_KEYWORDS = {
_FD.TYPE_STRING: "string",
_FD.TYPE_INT32: "int32",
_FD.TYPE_INT64: "int64",
_FD.TYPE_UINT32: "uint32",
_FD.TYPE_UINT64: "uint64",
_FD.TYPE_DOUBLE: "double",
_FD.TYPE_FLOAT: "float",
_FD.TYPE_BOOL: "bool",
_FD.TYPE_BYTES: "bytes",
}
def _unqualified(type_name: str) -> str:
"""".package.Message" (FileDescriptorProto's fully-qualified form) ->
"Message" - every message schema.py generates lives in one flat file
(no message is physically nested inside another), so an unqualified
reference is always enough within that one file."""
return type_name.rsplit(".", 1)[-1]
def _field_type_name(field: descriptor_pb2.FieldDescriptorProto) -> str:
if field.type in (_FD.TYPE_MESSAGE, _FD.TYPE_ENUM):
return _unqualified(field.type_name)
return _SCALAR_KEYWORDS[field.type]
def _render_message(message: descriptor_pb2.DescriptorProto) -> list[str]:
lines = [f"message {message.name} {{"]
# schema.py's _add_field() gives every proto3-`optional` field its own
# synthetic one-field oneof named "_<field>" (the standard proto3
# encoding of `optional`) - rendered as a plain `optional` field, the
# same way protoc itself prints that construct back, not as its own
# explicit oneof block. Only a "real", multi-field oneof (currently:
# ClientMessage/ServerMessage's "event" oneof) gets a rendered block.
synthetic_oneofs = {i for i, o in enumerate(message.oneof_decl) if o.name.startswith("_")}
for field in message.field:
if field.HasField("oneof_index") and field.oneof_index not in synthetic_oneofs:
continue # rendered below, inside its oneof block
label = ""
if field.label == _FD.LABEL_REPEATED:
label = "repeated "
elif field.proto3_optional:
label = "optional "
lines.append(f" {label}{_field_type_name(field)} {field.name} = {field.number};")
for index, oneof in enumerate(message.oneof_decl):
if index in synthetic_oneofs:
continue
lines.append(f" oneof {oneof.name} {{")
for field in message.field:
if field.HasField("oneof_index") and field.oneof_index == index:
lines.append(f" {_field_type_name(field)} {field.name} = {field.number};")
lines.append(" }")
lines.append("}")
return lines
def _render_service(service: descriptor_pb2.ServiceDescriptorProto) -> list[str]:
lines = [f"service {service.name} {{"]
for method in service.method:
client_stream = "stream " if method.client_streaming else ""
server_stream = "stream " if method.server_streaming else ""
input_type = _unqualified(method.input_type)
output_type = _unqualified(method.output_type)
lines.append(f" rpc {method.name} ({client_stream}{input_type}) returns ({server_stream}{output_type});")
lines.append("}")
return lines
[docs]
def render_proto(file_proto: descriptor_pb2.FileDescriptorProto) -> str:
"""Render a FileDescriptorProto (as produced by
edri.api.grpc.schema.build_schema()) to .proto3 source text."""
lines = ['syntax = "proto3";', "", f"package {file_proto.package};", ""]
for dependency in file_proto.dependency:
lines.append(f'import "{dependency}";')
if file_proto.dependency:
lines.append("")
for message in file_proto.message_type:
lines.extend(_render_message(message))
lines.append("")
for service in file_proto.service:
lines.extend(_render_service(service))
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def main(argv: list[str] | None = None) -> None:
argv = sys.argv[1:] if argv is None else argv
text = render_proto(build_schema().file_descriptor_proto)
if argv:
with open(argv[0], "w") as out:
out.write(text)
else:
sys.stdout.write(text)
if __name__ == "__main__":
main()