client

package
v0.0.0-...-e238554 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 30, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package client provides a Go client for the python-executor service.

The python-executor service allows remote execution of Python code in isolated Docker containers. This client handles all the complexity of the API, including tar archive creation, multipart requests, and response parsing.

Quick Start

c := client.New("http://pyexec.cluster:9999/")

tarData, _ := client.TarFromMap(map[string]string{
    "main.py": `print("Hello from Go!")`,
})

result, err := c.ExecuteSync(context.Background(), tarData, &client.Metadata{
    Entrypoint: "main.py",
})
if err != nil {
    log.Fatal(err)
}

fmt.Println(result.Stdout)

Creating Tar Archives

Several helper functions are provided for creating tar archives:

  • TarFromMap: Create from a map of filename to content
  • TarFromFiles: Create from a list of file paths
  • TarFromDirectory: Create from a directory path
  • TarFromReader: Create from an io.Reader (e.g., stdin)
Example (AsyncExecution)

Example_asyncExecution demonstrates asynchronous execution. This example requires a running server.

package main

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func getServerURL() string {
	if url := os.Getenv("PYEXEC_SERVER"); url != "" {
		return url
	}
	return "http://pyexec.cluster:9999/"
}

func main() {
	c := client.New(getServerURL())

	tarData, _ := client.TarFromMap(map[string]string{
		"main.py": `
import time
time.sleep(1)
print("Async complete!")
`,
	})

	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	// Submit asynchronously
	execID, err := c.ExecuteAsync(ctx, tarData, &client.Metadata{
		Entrypoint: "main.py",
	})
	if err != nil {
		fmt.Printf("Error submitting: %v\n", err)
		return
	}

	fmt.Printf("Submitted execution ID received: %t\n", strings.HasPrefix(execID, "exe_"))

	// Wait for completion
	result, err := c.WaitForCompletion(ctx, execID, 500*time.Millisecond)
	if err != nil {
		fmt.Printf("Error waiting: %v\n", err)
		return
	}

	fmt.Printf("Status: %s\n", result.Status)
	fmt.Printf("Output: %s", strings.TrimSpace(result.Stdout))
}
Output:

Submitted execution ID received: true
Status: completed
Output: Async complete!
Example (EvalMultiLine)

Example_evalMultiLine demonstrates REPL evaluation with multi-line code. This example requires a running server.

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func getServerURL() string {
	if url := os.Getenv("PYEXEC_SERVER"); url != "" {
		return url
	}
	return "http://pyexec.cluster:9999/"
}

func main() {
	c := client.New(getServerURL())

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Multi-line code with variable assignment and expression
	result, err := c.Eval(ctx, &client.SimpleExecRequest{
		Code: `x = 10
y = 5
x * y`,
		EvalLastExpr: true,
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Exit code: %d\n", result.ExitCode)
	if result.Result != nil {
		fmt.Printf("Result: %s\n", *result.Result)
	}
}
Output:

Exit code: 0
Result: 50
Example (EvalSimple)

Example_evalSimple demonstrates simple REPL-style evaluation. This example requires a running server.

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func getServerURL() string {
	if url := os.Getenv("PYEXEC_SERVER"); url != "" {
		return url
	}
	return "http://pyexec.cluster:9999/"
}

func main() {
	c := client.New(getServerURL())

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Simple expression evaluation
	result, err := c.Eval(ctx, &client.SimpleExecRequest{
		Code:         "2 + 2",
		EvalLastExpr: true,
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Exit code: %d\n", result.ExitCode)
	if result.Result != nil {
		fmt.Printf("Result: %s\n", *result.Result)
	}
}
Output:

Exit code: 0
Result: 4
Example (EvalWithImport)

Example_evalWithImport demonstrates REPL evaluation with imports. This example requires a running server.

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func getServerURL() string {
	if url := os.Getenv("PYEXEC_SERVER"); url != "" {
		return url
	}
	return "http://pyexec.cluster:9999/"
}

func main() {
	c := client.New(getServerURL())

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Import a module and evaluate an expression
	result, err := c.Eval(ctx, &client.SimpleExecRequest{
		Code:         "import math\nmath.sqrt(16)",
		EvalLastExpr: true,
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Exit code: %d\n", result.ExitCode)
	if result.Result != nil {
		fmt.Printf("Result: %s\n", *result.Result)
	}
}
Output:

Exit code: 0
Result: 4.0
Example (EvalWithPrint)

Example_evalWithPrint demonstrates that print statements produce stdout while expressions produce results. This example requires a running server.

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func getServerURL() string {
	if url := os.Getenv("PYEXEC_SERVER"); url != "" {
		return url
	}
	return "http://pyexec.cluster:9999/"
}

func main() {
	c := client.New(getServerURL())

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Print produces stdout, not a result
	result, err := c.Eval(ctx, &client.SimpleExecRequest{
		Code:         `print("Hello from eval!")`,
		EvalLastExpr: true,
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Exit code: %d\n", result.ExitCode)
	fmt.Printf("Stdout: %s", result.Stdout)
	if result.Result != nil && *result.Result != "" {
		fmt.Printf("Result: %s\n", *result.Result)
	} else {
		fmt.Println("Result: (none)")
	}
}
Output:

Exit code: 0
Stdout: Hello from eval!
Result: (none)
Example (ExecuteSync)

Example_executeSync demonstrates synchronous execution. This example requires a running server.

package main

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func getServerURL() string {
	if url := os.Getenv("PYEXEC_SERVER"); url != "" {
		return url
	}
	return "http://pyexec.cluster:9999/"
}

func main() {
	c := client.New(getServerURL())

	tarData, err := client.TarFromMap(map[string]string{
		"main.py": `print("Hello, World!")`,
	})
	if err != nil {
		fmt.Printf("Error creating tar: %v\n", err)
		return
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	result, err := c.ExecuteSync(ctx, tarData, &client.Metadata{
		Entrypoint: "main.py",
	})
	if err != nil {
		fmt.Printf("Error executing: %v\n", err)
		return
	}

	fmt.Printf("Exit code: %d\n", result.ExitCode)
	fmt.Printf("Output: %s", strings.TrimSpace(result.Stdout))
}
Output:

Exit code: 0
Output: Hello, World!
Example (ExecuteWithConfig)

Example_executeWithConfig demonstrates execution with custom configuration. This example requires a running server.

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func getServerURL() string {
	if url := os.Getenv("PYEXEC_SERVER"); url != "" {
		return url
	}
	return "http://pyexec.cluster:9999/"
}

func main() {
	c := client.New(getServerURL())

	tarData, _ := client.TarFromMap(map[string]string{
		"main.py": `
import sys
print(f"Args: {sys.argv[1:]}")
print("Config test passed")
`,
	})

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	result, err := c.ExecuteSync(ctx, tarData, &client.Metadata{
		Entrypoint: "main.py",
		ScriptArgs: []string{"--verbose", "test.txt"},
		Config: &client.ExecutionConfig{
			TimeoutSeconds:  60,
			NetworkDisabled: true,
			MemoryMB:        512,
		},
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Exit code: %d\n", result.ExitCode)
}
Output:

Exit code: 0
Example (MultiFileExecution)

Example_multiFileExecution demonstrates executing multiple files. This example requires a running server.

package main

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func getServerURL() string {
	if url := os.Getenv("PYEXEC_SERVER"); url != "" {
		return url
	}
	return "http://pyexec.cluster:9999/"
}

func main() {
	c := client.New(getServerURL())

	tarData, _ := client.TarFromMap(map[string]string{
		"main.py": `
from helper import calculate
result = calculate(5, 3)
print(f"Result: {result}")
`,
		"helper.py": `
def calculate(a, b):
    return a * b
`,
	})

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	result, err := c.ExecuteSync(ctx, tarData, &client.Metadata{
		Entrypoint: "main.py",
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Output: %s", strings.TrimSpace(result.Stdout))
}
Output:

Output: Result: 15

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func DetectEntrypoint

func DetectEntrypoint(tarData []byte) (string, error)

DetectEntrypoint finds the Python entrypoint in a tar archive.

The detection order is:

  1. main.py (highest priority)
  2. __main__.py
  3. First .py file found

Returns an error if no Python files are found.

Example

ExampleDetectEntrypoint demonstrates automatic entrypoint detection.

package main

import (
	"fmt"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func main() {
	// Archive with main.py
	tarData, _ := client.TarFromMap(map[string]string{
		"main.py":   `print("main")`,
		"helper.py": `print("helper")`,
	})

	entrypoint, err := client.DetectEntrypoint(tarData)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Detected entrypoint: %s\n", entrypoint)
}
Output:

Detected entrypoint: main.py

func TarFromDirectory

func TarFromDirectory(dirPath string) ([]byte, error)

TarFromDirectory creates an uncompressed tar archive from a directory.

All files in the directory are added recursively with relative paths.

Example:

tarData, err := client.TarFromDirectory("./myproject")

func TarFromFiles

func TarFromFiles(files []string) ([]byte, error)

TarFromFiles creates an uncompressed tar archive from a list of file paths.

Each file is added to the archive with its base name (not the full path).

Example:

tarData, err := client.TarFromFiles([]string{
    "main.py",
    "utils.py",
    "config.json",
})

func TarFromMap

func TarFromMap(files map[string]string) ([]byte, error)

TarFromMap creates a tar archive from a map of filename to content.

This is the most convenient way to create a tar archive for simple scripts.

Example:

tarData, err := client.TarFromMap(map[string]string{
    "main.py":   `print("Hello!")`,
    "helper.py": `def greet(): print("Hi!")`,
})
Example

ExampleTarFromMap demonstrates creating a tar archive from a map.

package main

import (
	"fmt"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func main() {
	files := map[string]string{
		"main.py":   `print("Hello from main!")`,
		"helper.py": `def greet(): return "Hi!"`,
	}

	tarData, err := client.TarFromMap(files)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Created tar archive: %t\n", len(tarData) > 0)
}
Output:

Created tar archive: true

func TarFromReader

func TarFromReader(r io.Reader, filename string) ([]byte, error)

TarFromReader creates a tar archive from an io.Reader (e.g., stdin).

The content is read entirely and stored in the archive under the given filename.

Example:

tarData, err := client.TarFromReader(os.Stdin, "main.py")

Types

type AsyncResponse

type AsyncResponse struct {
	ExecutionID string `json:"execution_id"`
}

AsyncResponse is returned when submitting async execution.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the Go client for the python-executor service.

Create a new client with New and use methods like Client.ExecuteSync and Client.ExecuteAsync to execute Python code remotely.

func New

func New(baseURL string, opts ...Option) *Client

New creates a new python-executor client.

The baseURL should point to the python-executor server, e.g., "http://pyexec.cluster:9999/" or "http://localhost:8080".

Options can be used to customize the client:

c := client.New("http://localhost:8080",
    client.WithTimeout(60 * time.Second),
)
Example

ExampleNew demonstrates creating a new client.

package main

import (
	"fmt"
	"time"

	"github.com/geraldthewes/python-executor/pkg/client"
)

func main() {
	// Create a client with default settings
	c := client.New("http://pyexec.cluster:9999/")
	_ = c

	// Create a client with custom timeout
	c2 := client.New("http://localhost:8080",
		client.WithTimeout(60*time.Second),
	)
	_ = c2

	fmt.Println("Clients created")
}
Output:

Clients created

func (*Client) Eval

Eval executes Python code using the simplified JSON API with REPL-style evaluation.

This method uses the /api/v1/eval endpoint which accepts JSON instead of multipart/form-data with tar archives, making it ideal for simple code execution.

When EvalLastExpr is true (the default for this endpoint), the last expression in the code will be evaluated and its value returned in the Result field.

Example:

result, err := c.Eval(ctx, &client.SimpleExecRequest{
    Code: "x = 5\nx * 2",
    EvalLastExpr: true,
})
if err != nil {
    return err
}
fmt.Println(*result.Result)  // Output: 10

func (*Client) ExecuteAsync

func (c *Client) ExecuteAsync(ctx context.Context, tarData []byte, metadata *Metadata) (string, error)

ExecuteAsync submits Python code for asynchronous execution.

Returns an execution ID immediately. Use Client.GetExecution to check status or Client.WaitForCompletion to wait for the result.

Example:

execID, err := c.ExecuteAsync(ctx, tarData, &client.Metadata{
    Entrypoint: "main.py",
})
if err != nil {
    return err
}

// Later, wait for completion
result, err := c.WaitForCompletion(ctx, execID, 2*time.Second)

func (*Client) ExecuteSync

func (c *Client) ExecuteSync(ctx context.Context, tarData []byte, metadata *Metadata) (*ExecutionResult, error)

ExecuteSync executes Python code and waits for the result.

This method blocks until execution completes. Use Client.ExecuteAsync for long-running scripts.

Example:

tarData, _ := client.TarFromMap(map[string]string{
    "main.py": `print("Hello!")`,
})

result, err := c.ExecuteSync(ctx, tarData, &client.Metadata{
    Entrypoint: "main.py",
})
if err != nil {
    return err
}

fmt.Printf("Exit code: %d\n", result.ExitCode)
fmt.Printf("Output: %s\n", result.Stdout)

func (*Client) GetExecution

func (c *Client) GetExecution(ctx context.Context, executionID string) (*ExecutionResult, error)

GetExecution retrieves the current status and result of an execution.

Returns the execution status which may be pending, running, completed, failed, or killed. Once completed, the result includes stdout, stderr, and exit code.

func (*Client) KillExecution

func (c *Client) KillExecution(ctx context.Context, executionID string) error

KillExecution terminates a running execution.

The Docker container running the Python code will be forcefully stopped.

func (*Client) WaitForCompletion

func (c *Client) WaitForCompletion(ctx context.Context, executionID string, pollInterval time.Duration) (*ExecutionResult, error)

WaitForCompletion polls the server until the execution completes.

The method polls at the specified interval until the execution reaches a terminal state (completed, failed, or killed).

Example:

execID, _ := c.ExecuteAsync(ctx, tarData, metadata)

// Poll every 2 seconds
result, err := c.WaitForCompletion(ctx, execID, 2*time.Second)
if err != nil {
    return err
}

fmt.Println(result.Stdout)

type CodeFile

type CodeFile struct {
	Name    string `json:"name"`    // filename (e.g., "main.py")
	Content string `json:"content"` // file content
}

CodeFile represents a single file with its content

type ExecutionConfig

type ExecutionConfig struct {
	// TimeoutSeconds is the maximum execution time (default: 300).
	TimeoutSeconds int `json:"timeout_seconds,omitempty"`
	// NetworkDisabled disables network access if true (default: true).
	NetworkDisabled bool `json:"network_disabled,omitempty"`
	// MemoryMB is the memory limit in megabytes (default: 1024).
	MemoryMB int `json:"memory_mb,omitempty"`
	// DiskMB is the disk space limit in megabytes (default: 2048).
	DiskMB int `json:"disk_mb,omitempty"`
	// CPUShares is the CPU shares (relative weight, default: 1024).
	CPUShares int `json:"cpu_shares,omitempty"`
}

ExecutionConfig holds resource limits and execution settings.

Example:

config := &client.ExecutionConfig{
    TimeoutSeconds:  60,
    NetworkDisabled: false,
    MemoryMB:        2048,
}

type ExecutionResult

type ExecutionResult struct {
	// ExecutionID is the unique identifier for this execution.
	ExecutionID string `json:"execution_id"`
	// Status is the current execution state.
	Status ExecutionStatus `json:"status"`
	// Stdout is the standard output from the Python script.
	Stdout string `json:"stdout,omitempty"`
	// Stderr is the standard error from the Python script.
	Stderr string `json:"stderr,omitempty"`
	// ExitCode is the process exit code (0 = success).
	ExitCode int `json:"exit_code"`
	// Error is an error message if the execution failed internally.
	Error string `json:"error,omitempty"`
	// ErrorType is the Python exception type (e.g., "SyntaxError", "NameError").
	ErrorType string `json:"error_type,omitempty"`
	// ErrorLine is the line number where the error occurred.
	ErrorLine int `json:"error_line,omitempty"`
	// StartedAt is when execution started (UTC).
	StartedAt *time.Time `json:"started_at,omitempty"`
	// FinishedAt is when execution finished (UTC).
	FinishedAt *time.Time `json:"finished_at,omitempty"`
	// DurationMs is the total execution time in milliseconds.
	DurationMs int64 `json:"duration_ms,omitempty"`
	// Result contains the value of the last expression when EvalLastExpr is true.
	// The value is the repr() of the Python object, or null if the last
	// statement was not an expression.
	Result *string `json:"result,omitempty"`
}

ExecutionResult contains the output and status of an execution.

type ExecutionStatus

type ExecutionStatus string

ExecutionStatus represents the status of a code execution.

const (
	// StatusPending indicates the execution is queued but not yet started.
	StatusPending ExecutionStatus = "pending"
	// StatusRunning indicates the execution is currently in progress.
	StatusRunning ExecutionStatus = "running"
	// StatusCompleted indicates the execution finished (check ExitCode for success).
	StatusCompleted ExecutionStatus = "completed"
	// StatusFailed indicates the execution failed due to an internal error.
	StatusFailed ExecutionStatus = "failed"
	// StatusKilled indicates the execution was terminated by the user.
	StatusKilled ExecutionStatus = "killed"
)

Execution status constants.

type KillResponse

type KillResponse struct {
	Status string `json:"status"`
}

KillResponse is returned when killing an execution.

type Metadata

type Metadata struct {
	// Entrypoint is the Python file to execute (required).
	Entrypoint string `json:"entrypoint"`
	// DockerImage is the Docker image to use (default: python:3.11-slim).
	DockerImage string `json:"docker_image,omitempty"`
	// RequirementsTxt is the contents of requirements.txt for pip install.
	RequirementsTxt string `json:"requirements_txt,omitempty"`
	// PreCommands are shell commands to run before Python execution.
	PreCommands []string `json:"pre_commands,omitempty"`
	// Stdin is data to provide on standard input.
	Stdin string `json:"stdin,omitempty"`
	// Config contains resource limits and settings.
	Config *ExecutionConfig `json:"config,omitempty"`
	// EnvVars are environment variables in "KEY=value" format.
	EnvVars []string `json:"env_vars,omitempty"`
	// ScriptArgs are arguments passed to the Python script (sys.argv).
	ScriptArgs []string `json:"script_args,omitempty"`
	// EvalLastExpr enables REPL-style behavior for simple execution.
	// Internal use only - set via SimpleExecRequest.EvalLastExpr.
	EvalLastExpr bool `json:"-"`
}

Metadata contains execution parameters sent to the server.

At minimum, Entrypoint must be specified. All other fields are optional.

Example:

metadata := &client.Metadata{
    Entrypoint:      "main.py",
    RequirementsTxt: "requests\nnumpy",
    Config: &client.ExecutionConfig{
        NetworkDisabled: false,  // Allow network for pip
    },
}

type Option

type Option func(*Client)

Option is a functional option for configuring the Client.

func WithHTTPClient

func WithHTTPClient(httpClient *http.Client) Option

WithHTTPClient sets a custom HTTP client.

Use this to configure custom transport settings, proxies, or TLS configuration.

Example:

transport := &http.Transport{
    TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: transport}
c := client.New(url, client.WithHTTPClient(httpClient))

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout sets the HTTP client timeout.

The default timeout is 5 minutes.

Example:

c := client.New(url, client.WithTimeout(60*time.Second))

type SimpleExecRequest

type SimpleExecRequest struct {
	// Code is the source code to execute (for single-file execution)
	// If provided, creates a main.py with this content
	Code string `json:"code,omitempty"`

	// Files allows multiple files to be provided (Piston-compatible)
	// Takes precedence over Code if both are provided
	Files []CodeFile `json:"files,omitempty"`

	// Entrypoint is the file to execute (defaults to "main.py")
	Entrypoint string `json:"entrypoint,omitempty"`

	// Stdin is the standard input to provide to the script
	Stdin string `json:"stdin,omitempty"`

	// Config contains execution resource limits
	Config *ExecutionConfig `json:"config,omitempty"`

	// PythonVersion specifies the Python version to use (e.g., "3.10", "3.11", "3.12", "3.13")
	// If not specified, uses the server default (typically 3.12)
	PythonVersion string `json:"python_version,omitempty"`

	// EvalLastExpr enables REPL-style behavior: if the last statement is an
	// expression, its value is captured and returned in the Result field.
	// Only applies to single-file code execution.
	EvalLastExpr bool `json:"eval_last_expr,omitempty"`

	// RequirementsTxt allows explicit package specification.
	// These are merged with auto-detected packages (user-provided takes precedence).
	// Set to "-" to disable auto-detection entirely for this request.
	RequirementsTxt string `json:"requirements_txt,omitempty"`
}

SimpleExecRequest is the JSON-only execution request format Compatible with Replit/Piston-style APIs for simpler integrations

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL