feat: add optional Python gRPC server and proto scaffolding (#14) - #15
Open
antoinetoussaint-byte wants to merge 2 commits into
Open
feat: add optional Python gRPC server and proto scaffolding (#14)#15antoinetoussaint-byte wants to merge 2 commits into
antoinetoussaint-byte wants to merge 2 commits into
Conversation
Add an opt-in, service-owned grpc.aio listener to the generated FastAPI service. It runs in the same process as the FastAPI app (booted from the lifespan), on its own internal port, and stays entirely off by default so existing REST-only services and their generated layout are unchanged. - grpc-server settings (enabled/proto) + a creation question, disabled by default. - Scaffold proto/api.proto, Buf config, a grpc.aio server, a gRPC health service, and a user-owned servicer seam when enabled. - Regenerate the Python protobuf + gRPC stubs from the proto during Sync. - Create/load a Codefly gRPC endpoint in Builder and Runtime; wire native, container, and Kubernetes port mappings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-safe (#14) Addresses review findings on the optional gRPC server. #1 (main.py): the grpc server was imported at module top level, so importing src.main required the generated stubs. Runtime.Init runs GenerateOpenAPI, which imports src.main *before* Sync generates those stubs (and they are git-ignored, so a fresh clone has none) — Init failed with ModuleNotFoundError. The import now lives inside the startup handler; OpenAPI generation never fires startup, so the module imports cleanly without stubs. #2 (proto location): the proto was scaffolded at code/proto/api.proto, but core's LoadEndpoints re-derives the gRPC contract from standards.ProtoPath (proto/api.proto at the service root) and the manifest stores no proto bytes. The endpoint therefore reloaded with zero RPCs and dependent services could not generate clients. The proto now lives at the service-root standard path (Buf and grpcEndpoint read it there), and defaultProtoPath is bound to standards.ProtoPath so it can never drift again. #3 (server.py): add_insecure_port returns 0 instead of raising when a port can't be bound, so the server would "start" listening on nothing while FastAPI stayed healthy. It now raises, which also fails the pod's HTTP readiness probe via the shared lifespan (covers the gRPC-readiness gap). Tests: added a reload regression test asserting the gRPC endpoint keeps its RPCs after LoadEndpoints (would have caught #2), and a check that src.rpc is never imported at module top level (would have caught #1). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #14.
Summary
grpc.aiolistener runs in the same process as the FastAPI app (booted from and stopped with the FastAPI lifespan) on its own internal port, so protobuf becomes the canonical typed contract while FastAPI stays the ASGI/REST listener.grpc-serverblock, the generated service and its layout are byte-for-byte unchanged (TestCreateRESTOnlyByDefault, and the existing REST create-to-run test still passes on the default path).What's included (issue's "Optional Python gRPC server" section)
grpc-serversettings (enabled,proto) inline-embedded inSettings, plus a creation question defaulting to no.code/proto/api.proto, Buf config, agrpc.aioserver, a gRPC health service, a user-owned servicer seam (src/rpc/servicer.py, never overwritten) separate from generator-owned files, and a round-trip test — only when enabled.proto.NewBuf), deterministically (cached on the proto tree).CODEFLY_GRPC_PORTfor native/container, a fixedcontainerPort/Service port for k8s).Protocol_GRPCas an agent capability; graceful shutdown releases both listeners via the lifespan.Deferred to follow-ups (not in this PR)
Issue #14 is an epic spanning three subsystems plus several explicitly-external prerequisites; landing it as one PR would not be one reviewable logical change. This PR implements the gRPC server foundation — the issue's "first required path" that the rest builds on. The following are not implemented here and should be tracked as follow-up PRs on top of this one:
tools/list/tools/callround-trip).operationId/x-mcpselection, dispatch through FastAPI dependencies).Test plan
go build ./...andgofmtclean.go test ./...passes exceptTestCreateToRunDocker, which requires a Docker daemon not available in this environment (fails identically onmain— unrelated to this change).buf generateandgrpc_tools.protocboth produce the stubs; the scaffoldedgrpc.aioserver answers an Echo RPC and the healthCheckreturnsSERVING(tests/rpc/test_grpc.py, run viauv).