88 "log"
99 "os"
1010 "path/filepath"
11+ "sort"
1112 "strings"
1213 "text/template"
1314 "time"
@@ -18,8 +19,36 @@ func main() {
1819 goOut := flag .String ("go-out" , "" , "output directory for Go SDK" )
1920 pythonOut := flag .String ("python-out" , "" , "output directory for Python SDK" )
2021 tsOut := flag .String ("ts-out" , "" , "output directory for TypeScript SDK" )
22+ protoPath := flag .String ("proto" , "" , "path to .proto file (required for --grpc-python-out)" )
23+ grpcPythonOut := flag .String ("grpc-python-out" , "" , "output directory for Python gRPC client" )
2124 flag .Parse ()
2225
26+ if * grpcPythonOut != "" {
27+ if * protoPath == "" {
28+ log .Fatal ("--proto is required when --grpc-python-out is set" )
29+ }
30+ protoSpec , err := parseProto (* protoPath )
31+ if err != nil {
32+ log .Fatalf ("parse proto: %v" , err )
33+ }
34+ protoHash , err := hashFile (* protoPath )
35+ if err != nil {
36+ log .Fatalf ("hash proto: %v" , err )
37+ }
38+ header := ProtoGeneratedHeader {
39+ ProtoPath : * protoPath ,
40+ ProtoHash : protoHash ,
41+ Timestamp : time .Now ().UTC ().Format (time .RFC3339 ),
42+ }
43+ if err := generateGRPCPython (protoSpec , * grpcPythonOut , header ); err != nil {
44+ log .Fatalf ("generate gRPC Python: %v" , err )
45+ }
46+ fmt .Printf ("Python gRPC client generated in %s\n " , * grpcPythonOut )
47+ if * specPath == "" {
48+ return
49+ }
50+ }
51+
2352 if * specPath == "" {
2453 log .Fatal ("--spec is required" )
2554 }
@@ -79,6 +108,165 @@ type GeneratedHeader struct {
79108 Timestamp string
80109}
81110
111+ type ProtoGeneratedHeader struct {
112+ ProtoPath string
113+ ProtoHash string
114+ Timestamp string
115+ }
116+
117+ type ProtoRPC struct {
118+ Name string
119+ InputType string
120+ OutputType string
121+ ServerStreaming bool
122+ }
123+
124+ type ProtoService struct {
125+ Name string
126+ Package string
127+ RPCs []ProtoRPC
128+ }
129+
130+ type ProtoSpec struct {
131+ Service ProtoService
132+ }
133+
134+ type grpcPythonTemplateData struct {
135+ Header ProtoGeneratedHeader
136+ Service ProtoService
137+ Spec * ProtoSpec
138+ }
139+
140+ func parseProto (path string ) (* ProtoSpec , error ) {
141+ data , err := os .ReadFile (path )
142+ if err != nil {
143+ return nil , fmt .Errorf ("read proto: %w" , err )
144+ }
145+ content := string (data )
146+
147+ pkg := ""
148+ for _ , line := range strings .Split (content , "\n " ) {
149+ line = strings .TrimSpace (line )
150+ if strings .HasPrefix (line , "package " ) {
151+ pkg = strings .TrimSuffix (strings .TrimPrefix (line , "package " ), ";" )
152+ pkg = strings .TrimSpace (pkg )
153+ break
154+ }
155+ }
156+
157+ var serviceName string
158+ var rpcs []ProtoRPC
159+ inService := false
160+ for _ , line := range strings .Split (content , "\n " ) {
161+ trimmed := strings .TrimSpace (line )
162+ if ! inService {
163+ if strings .HasPrefix (trimmed , "service " ) {
164+ parts := strings .Fields (trimmed )
165+ if len (parts ) >= 2 {
166+ serviceName = parts [1 ]
167+ }
168+ inService = true
169+ }
170+ continue
171+ }
172+ if trimmed == "}" {
173+ inService = false
174+ continue
175+ }
176+ if strings .HasPrefix (trimmed , "rpc " ) {
177+ rpc := parseRPCLine (trimmed )
178+ if rpc != nil {
179+ rpcs = append (rpcs , * rpc )
180+ }
181+ }
182+ }
183+
184+ return & ProtoSpec {
185+ Service : ProtoService {
186+ Name : serviceName ,
187+ Package : pkg ,
188+ RPCs : rpcs ,
189+ },
190+ }, nil
191+ }
192+
193+ func parseRPCLine (line string ) * ProtoRPC {
194+ line = strings .TrimPrefix (line , "rpc " )
195+ parenIdx := strings .Index (line , "(" )
196+ if parenIdx < 0 {
197+ return nil
198+ }
199+ name := strings .TrimSpace (line [:parenIdx ])
200+ rest := line [parenIdx + 1 :]
201+ closeIdx := strings .Index (rest , ")" )
202+ if closeIdx < 0 {
203+ return nil
204+ }
205+ inputType := strings .TrimSpace (rest [:closeIdx ])
206+ rest = rest [closeIdx + 1 :]
207+ returnsIdx := strings .Index (rest , "returns" )
208+ if returnsIdx < 0 {
209+ return nil
210+ }
211+ rest = rest [returnsIdx + len ("returns" ):]
212+ serverStreaming := strings .Contains (rest , "stream" )
213+ rest = strings .ReplaceAll (rest , "stream" , "" )
214+ openParen := strings .Index (rest , "(" )
215+ closeParen := strings .Index (rest , ")" )
216+ if openParen < 0 || closeParen < 0 {
217+ return nil
218+ }
219+ outputType := strings .TrimSpace (rest [openParen + 1 : closeParen ])
220+ return & ProtoRPC {
221+ Name : name ,
222+ InputType : inputType ,
223+ OutputType : outputType ,
224+ ServerStreaming : serverStreaming ,
225+ }
226+ }
227+
228+ func hashFile (path string ) (string , error ) {
229+ h := sha256 .New ()
230+ f , err := os .Open (path )
231+ if err != nil {
232+ return "" , err
233+ }
234+ defer func () { _ = f .Close () }()
235+ if _ , err := io .Copy (h , f ); err != nil {
236+ return "" , err
237+ }
238+ return fmt .Sprintf ("%x" , h .Sum (nil )), nil
239+ }
240+
241+ func generateGRPCPython (spec * ProtoSpec , outDir string , header ProtoGeneratedHeader ) error {
242+ if err := os .MkdirAll (outDir , 0755 ); err != nil {
243+ return err
244+ }
245+
246+ tmplDir := filepath .Join (getTemplateDir (), "grpc" , "python" )
247+ data := grpcPythonTemplateData {Header : header , Service : spec .Service , Spec : spec }
248+
249+ files := []struct {
250+ tmpl string
251+ out string
252+ }{
253+ {"grpc_client.py.tmpl" , "_grpc_client.py" },
254+ {"messages_api.py.tmpl" , "_session_messages_api.py" },
255+ }
256+
257+ for _ , f := range files {
258+ tmpl , err := loadTemplate (filepath .Join (tmplDir , f .tmpl ))
259+ if err != nil {
260+ return fmt .Errorf ("load %s: %w" , f .tmpl , err )
261+ }
262+ if err := executeTemplate (tmpl , filepath .Join (outDir , f .out ), data ); err != nil {
263+ return fmt .Errorf ("execute %s: %w" , f .tmpl , err )
264+ }
265+ }
266+
267+ return nil
268+ }
269+
82270type goTemplateData struct {
83271 Header GeneratedHeader
84272 Resource Resource
@@ -356,13 +544,13 @@ func computeSpecHash(specPath string) (string, error) {
356544 specDir := filepath .Dir (specPath )
357545 h := sha256 .New ()
358546
359- files := []string {
360- specPath ,
361- filepath .Join (specDir , "openapi.sessions.yaml" ),
362- filepath .Join (specDir , "openapi.projects.yaml" ),
363- filepath .Join (specDir , "openapi.projectSettings.yaml" ),
364- filepath .Join (specDir , "openapi.users.yaml" ),
547+ subSpecs , err := filepath .Glob (filepath .Join (specDir , "openapi.*.yaml" ))
548+ if err != nil {
549+ return "" , fmt .Errorf ("glob sub-specs: %w" , err )
365550 }
551+ sort .Strings (subSpecs )
552+
553+ files := append ([]string {specPath }, subSpecs ... )
366554
367555 for _ , f := range files {
368556 fh , err := os .Open (f )
0 commit comments