Skip to content

Commit

Permalink
Merge pull request #28 from cpunion/cli
Browse files Browse the repository at this point in the history
cli: add gopy command
  • Loading branch information
cpunion authored Nov 20, 2024
2 parents 6d20a5c + 989ce2f commit 87c68df
Show file tree
Hide file tree
Showing 34 changed files with 2,921 additions and 28 deletions.
29 changes: 29 additions & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
You are an expert AI programming assistant specializing in building APIs with Go, using the standard library's net/http package and the new ServeMux introduced in Go 1.22.

Always use the latest stable version of Go (1.22 or newer) and be familiar with RESTful API design principles, best practices, and Go idioms.

- Follow the user's requirements carefully & to the letter.
- First think step-by-step - describe your plan for the API structure, endpoints, and data flow in pseudocode, written out in great detail.
- Confirm the plan, then write code!
- Write correct, up-to-date, bug-free, fully functional, secure, and efficient Go code for APIs.
- Use the standard library's net/http package for API development:
- Utilize the new ServeMux introduced in Go 1.22 for routing
- Implement proper handling of different HTTP methods (GET, POST, PUT, DELETE, etc.)
- Use method handlers with appropriate signatures (e.g., func(w http.ResponseWriter, r \*http.Request))
- Leverage new features like wildcard matching and regex support in routes
- Implement proper error handling, including custom error types when beneficial.
- Use appropriate status codes and format JSON responses correctly.
- Implement input validation for API endpoints.
- Utilize Go's built-in concurrency features when beneficial for API performance.
- Follow RESTful API design principles and best practices.
- Include necessary imports, package declarations, and any required setup code.
- Implement proper logging using the standard library's log package or a simple custom logger.
- Consider implementing middleware for cross-cutting concerns (e.g., logging, authentication).
- Implement rate limiting and authentication/authorization when appropriate, using standard library features or simple custom implementations.
- Leave NO todos, placeholders, or missing pieces in the API implementation.
- Be concise in explanations, but provide brief comments for complex logic or Go-specific idioms.
- Always use English in comments and code.
- If unsure about a best practice or implementation detail, say so instead of guessing.
- Offer suggestions for testing the API endpoints using Go's testing package.

Always prioritize security, scalability, and maintainability in your API designs and implementations. Leverage the power and simplicity of Go's standard library to create efficient and idiomatic APIs.
12 changes: 12 additions & 0 deletions .github/assets/python3-embed.pc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
prefix=${pcfiledir}/../..
exec_prefix=${prefix}
libdir=${exec_prefix}
includedir=${prefix}/include

Name: Python
Description: Embed Python into an application
Requires:
Version: 3.13
Libs.private:
Libs: -L${libdir} -lpython313
Cflags: -I${includedir}
55 changes: 49 additions & 6 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,69 @@ jobs:
strategy:
fail-fast: false
matrix:
os:
- macos-latest
- ubuntu-24.04
runs-on: ${{matrix.os}}
sys:
- {os: macos-latest, shell: bash}
- {os: ubuntu-24.04, shell: bash}
- {os: windows-latest, shell: bash}
defaults:
run:
shell: ${{ matrix.sys.shell }}
runs-on: ${{matrix.sys.os}}
steps:
# - uses: msys2/setup-msys2@v2
# if: matrix.sys.os == 'windows-latest'
# with:
# update: true
# install: >-
# curl
# git
# pkg-config

- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.23

- uses: actions/setup-python@v5
with:
python-version: '3.13'
update-environment: true

- name: Generate Python pkg-config for windows (patch)
if: matrix.sys.os == 'windows-latest'
run: |
mkdir -p $PKG_CONFIG_PATH
cp .github/assets/python3-embed.pc $PKG_CONFIG_PATH/
- name: Install tiny-pkg-config for windows (patch)
if: matrix.sys.os == 'windows-latest'
run: |
set -x
curl -L https://github.com/cpunion/tiny-pkg-config/releases/download/v0.2.0/tiny-pkg-config_Windows_x86_64.zip -o /tmp/tiny-pkg-config.zip
unzip /tmp/tiny-pkg-config.zip -d $HOME/bin
mv $HOME/bin/tiny-pkg-config.exe $HOME/bin/pkg-config.exe
echo $PKG_CONFIG_PATH
cat $PKG_CONFIG_PATH/python3-embed.pc
pkg-config --libs python3-embed
pkg-config --cflags python3-embed
- name: Build
run: go build -v ./...
run: go install -v ./...

- name: Test with coverage
run: go test -v -coverprofile=coverage.txt -covermode=atomic ./...

- name: Test gopy
run: |
gopy init $HOME/foo
cd $HOME/foo
gopy build -v .
gopy run -v .
gopy install -v .
- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-24.04'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
39 changes: 39 additions & 0 deletions cmd/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

// addCmd represents the add command
var addCmd = &cobra.Command{
Use: "add",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("add called")
},
}

func init() {
rootCmd.AddCommand(addCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// addCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
37 changes: 37 additions & 0 deletions cmd/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd

import (
"fmt"
"os"

"github.com/cpunion/go-python/cmd/internal/rungo"
"github.com/spf13/cobra"
)

// buildCmd represents the build command
var buildCmd = &cobra.Command{
Use: "build [flags] [package]",
Short: "Build a Go package with Python environment configured",
Long: func() string {
intro := "Build compiles a Go package with the Python environment properly configured.\n\n"
help, err := rungo.GetGoCommandHelp("build")
if err != nil {
return intro + "Failed to get go help: " + err.Error()
}
return intro + help
}(),
DisableFlagParsing: true,
Run: func(cmd *cobra.Command, args []string) {
if err := rungo.RunGoCommand("build", args); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
},
}

func init() {
rootCmd.AddCommand(buildCmd)
}
10 changes: 10 additions & 0 deletions cmd/gopy/gopy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package main

import "github.com/cpunion/go-python/cmd"

func main() {
cmd.Execute()
}
127 changes: 127 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd

import (
"bufio"
"fmt"
"io"
"os"
"strings"

"github.com/cpunion/go-python/cmd/internal/create"
"github.com/cpunion/go-python/cmd/internal/install"
"github.com/fatih/color"
"github.com/spf13/cobra"
)

var bold = color.New(color.Bold).SprintFunc()

// isDirEmpty checks if a directory is empty
func isDirEmpty(path string) (bool, error) {
f, err := os.Open(path)
if err != nil {
return false, err
}
defer f.Close()

_, err = f.Readdirnames(1)
if err == io.EOF {
return true, nil
}
return false, err
}

// promptYesNo asks user for confirmation
func promptYesNo(prompt string) bool {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("%s [y/N]: ", prompt)
response, err := reader.ReadString('\n')
if err != nil {
return false
}

response = strings.ToLower(strings.TrimSpace(response))
return response == "y" || response == "yes"
}

// initCmd represents the init command
var initCmd = &cobra.Command{
Use: "init [path]",
Short: "Initialize a new go-python project",
Long: `Initialize a new go-python project in the specified directory.
If no path is provided, it will initialize in the current directory.
Example:
gopy init
gopy init my-project
gopy init --debug my-project
gopy init -v my-project`,
Run: func(cmd *cobra.Command, args []string) {
// Get project path
projectPath := "."
if len(args) > 0 {
projectPath = args[0]
}

// Get flags
debug, _ := cmd.Flags().GetBool("debug")
verbose, _ := cmd.Flags().GetBool("verbose")
goVersion, _ := cmd.Flags().GetString("go-version")
pyVersion, _ := cmd.Flags().GetString("python-version")
pyBuildDate, _ := cmd.Flags().GetString("python-build-date")
pyFreeThreaded, _ := cmd.Flags().GetBool("python-free-threaded")
tinyPkgConfigVersion, _ := cmd.Flags().GetString("tiny-pkg-config-version")

// Check if directory exists
if _, err := os.Stat(projectPath); err == nil {
// Directory exists, check if it's empty
empty, err := isDirEmpty(projectPath)
if err != nil {
fmt.Printf("Error checking directory: %v\n", err)
return
}

if !empty {
if !promptYesNo(fmt.Sprintf("Directory %s is not empty. Do you want to continue?", projectPath)) {
fmt.Println("Operation cancelled")
return
}
}
} else if !os.IsNotExist(err) {
fmt.Printf("Error checking directory: %v\n", err)
return
}

// Create project using the create package
fmt.Printf("\n%s\n", bold("Creating project..."))
if err := create.Project(projectPath, verbose); err != nil {
fmt.Printf("Error creating project: %v\n", err)
return
}

// Install dependencies
fmt.Printf("\n%s\n", bold("Installing dependencies..."))
if err := install.Dependencies(projectPath, goVersion, tinyPkgConfigVersion, pyVersion, pyBuildDate, pyFreeThreaded, debug, verbose); err != nil {
fmt.Printf("Error installing dependencies: %v\n", err)
return
}

fmt.Printf("\n%s\n", bold("Successfully initialized go-python project in "+projectPath))
fmt.Println("\nNext steps:")
fmt.Println("1. cd", projectPath)
fmt.Println("2. gopy run .")
},
}

func init() {
rootCmd.AddCommand(initCmd)
initCmd.Flags().Bool("debug", false, "Install debug version of Python (not available on Windows)")
initCmd.Flags().BoolP("verbose", "v", false, "Enable verbose output")
initCmd.Flags().String("tiny-pkg-config-version", "v0.2.0", "tiny-pkg-config version to install")
initCmd.Flags().String("go-version", "1.23.3", "Go version to install")
initCmd.Flags().String("python-version", "3.13.0", "Python version to install")
initCmd.Flags().String("python-build-date", "20241016", "Python build date")
initCmd.Flags().Bool("python-free-threaded", false, "Install free-threaded version of Python")
}
37 changes: 37 additions & 0 deletions cmd/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd

import (
"fmt"
"os"

"github.com/cpunion/go-python/cmd/internal/rungo"
"github.com/spf13/cobra"
)

// installCmd represents the install command
var installCmd = &cobra.Command{
Use: "install [flags] [packages]",
Short: "Install Go packages with Python environment configured",
Long: func() string {
intro := "Install compiles and installs Go packages with the Python environment properly configured.\n\n"
help, err := rungo.GetGoCommandHelp("install")
if err != nil {
return intro + "Failed to get go help: " + err.Error()
}
return intro + help
}(),
DisableFlagParsing: true,
Run: func(cmd *cobra.Command, args []string) {
if err := rungo.RunGoCommand("install", args); err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
},
}

func init() {
rootCmd.AddCommand(installCmd)
}
Loading

0 comments on commit 87c68df

Please sign in to comment.