From ea5b2fabaffec2c5fcf2a6804522271115235530 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 29 Jan 2026 01:15:57 +0000 Subject: [PATCH] feat(sdk): add Python generator Implements Python SDK generator with two-level fallback: 1. Native openapi-python-client if installed 2. Docker openapitools/openapi-generator-cli as fallback Includes tests following _Good convention for Available and Generate. Co-Authored-By: Claude Opus 4.5 --- pkg/sdk/generators/python.go | 76 +++++++++++++++++++++++++++++++ pkg/sdk/generators/python_test.go | 58 +++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 pkg/sdk/generators/python.go create mode 100644 pkg/sdk/generators/python_test.go diff --git a/pkg/sdk/generators/python.go b/pkg/sdk/generators/python.go new file mode 100644 index 00000000..00db9c0a --- /dev/null +++ b/pkg/sdk/generators/python.go @@ -0,0 +1,76 @@ +package generators + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" +) + +// PythonGenerator generates Python SDKs from OpenAPI specs. +type PythonGenerator struct{} + +// NewPythonGenerator creates a new Python generator. +func NewPythonGenerator() *PythonGenerator { + return &PythonGenerator{} +} + +// Language returns the generator's target language identifier. +func (g *PythonGenerator) Language() string { + return "python" +} + +// Available checks if generator dependencies are installed. +func (g *PythonGenerator) Available() bool { + _, err := exec.LookPath("openapi-python-client") + return err == nil +} + +// Install returns instructions for installing the generator. +func (g *PythonGenerator) Install() string { + return "pip install openapi-python-client" +} + +// Generate creates SDK from OpenAPI spec. +func (g *PythonGenerator) Generate(ctx context.Context, opts Options) error { + if err := os.MkdirAll(opts.OutputDir, 0755); err != nil { + return fmt.Errorf("python.Generate: failed to create output dir: %w", err) + } + + if g.Available() { + return g.generateNative(ctx, opts) + } + return g.generateDocker(ctx, opts) +} + +func (g *PythonGenerator) generateNative(ctx context.Context, opts Options) error { + parentDir := filepath.Dir(opts.OutputDir) + + cmd := exec.CommandContext(ctx, "openapi-python-client", "generate", + "--path", opts.SpecPath, + "--output-path", opts.OutputDir, + ) + cmd.Dir = parentDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (g *PythonGenerator) generateDocker(ctx context.Context, opts Options) error { + specDir := filepath.Dir(opts.SpecPath) + specName := filepath.Base(opts.SpecPath) + + cmd := exec.CommandContext(ctx, "docker", "run", "--rm", + "-v", specDir+":/spec", + "-v", opts.OutputDir+":/out", + "openapitools/openapi-generator-cli", "generate", + "-i", "/spec/"+specName, + "-g", "python", + "-o", "/out", + "--additional-properties=packageName="+opts.PackageName, + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/pkg/sdk/generators/python_test.go b/pkg/sdk/generators/python_test.go new file mode 100644 index 00000000..5b03a768 --- /dev/null +++ b/pkg/sdk/generators/python_test.go @@ -0,0 +1,58 @@ +package generators + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" +) + +func TestPythonGenerator_Good_Available(t *testing.T) { + g := NewPythonGenerator() + + // These should not panic + lang := g.Language() + if lang != "python" { + t.Errorf("expected language 'python', got '%s'", lang) + } + + _ = g.Available() + + install := g.Install() + if install == "" { + t.Error("expected non-empty install instructions") + } +} + +func TestPythonGenerator_Good_Generate(t *testing.T) { + g := NewPythonGenerator() + if !g.Available() && !dockerAvailable() { + t.Skip("no Python generator available (neither native nor docker)") + } + + // Create temp directories + tmpDir := t.TempDir() + specPath := createTestSpec(t, tmpDir) + outputDir := filepath.Join(tmpDir, "output") + + opts := Options{ + SpecPath: specPath, + OutputDir: outputDir, + PackageName: "testclient", + Version: "1.0.0", + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + err := g.Generate(ctx, opts) + if err != nil { + t.Fatalf("Generate failed: %v", err) + } + + // Verify output directory was created + if _, err := os.Stat(outputDir); os.IsNotExist(err) { + t.Error("output directory was not created") + } +}