Skip to content

Commit 87c68df

Browse files
authored
Merge pull request #28 from cpunion/cli
cli: add gopy command
2 parents 6d20a5c + 989ce2f commit 87c68df

34 files changed

+2921
-28
lines changed

.cursorrules

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
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.
2+
3+
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.
4+
5+
- Follow the user's requirements carefully & to the letter.
6+
- First think step-by-step - describe your plan for the API structure, endpoints, and data flow in pseudocode, written out in great detail.
7+
- Confirm the plan, then write code!
8+
- Write correct, up-to-date, bug-free, fully functional, secure, and efficient Go code for APIs.
9+
- Use the standard library's net/http package for API development:
10+
- Utilize the new ServeMux introduced in Go 1.22 for routing
11+
- Implement proper handling of different HTTP methods (GET, POST, PUT, DELETE, etc.)
12+
- Use method handlers with appropriate signatures (e.g., func(w http.ResponseWriter, r \*http.Request))
13+
- Leverage new features like wildcard matching and regex support in routes
14+
- Implement proper error handling, including custom error types when beneficial.
15+
- Use appropriate status codes and format JSON responses correctly.
16+
- Implement input validation for API endpoints.
17+
- Utilize Go's built-in concurrency features when beneficial for API performance.
18+
- Follow RESTful API design principles and best practices.
19+
- Include necessary imports, package declarations, and any required setup code.
20+
- Implement proper logging using the standard library's log package or a simple custom logger.
21+
- Consider implementing middleware for cross-cutting concerns (e.g., logging, authentication).
22+
- Implement rate limiting and authentication/authorization when appropriate, using standard library features or simple custom implementations.
23+
- Leave NO todos, placeholders, or missing pieces in the API implementation.
24+
- Be concise in explanations, but provide brief comments for complex logic or Go-specific idioms.
25+
- Always use English in comments and code.
26+
- If unsure about a best practice or implementation detail, say so instead of guessing.
27+
- Offer suggestions for testing the API endpoints using Go's testing package.
28+
29+
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.

.github/assets/python3-embed.pc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
prefix=${pcfiledir}/../..
2+
exec_prefix=${prefix}
3+
libdir=${exec_prefix}
4+
includedir=${prefix}/include
5+
6+
Name: Python
7+
Description: Embed Python into an application
8+
Requires:
9+
Version: 3.13
10+
Libs.private:
11+
Libs: -L${libdir} -lpython313
12+
Cflags: -I${includedir}

.github/workflows/go.yml

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,26 +43,69 @@ jobs:
4343
strategy:
4444
fail-fast: false
4545
matrix:
46-
os:
47-
- macos-latest
48-
- ubuntu-24.04
49-
runs-on: ${{matrix.os}}
46+
sys:
47+
- {os: macos-latest, shell: bash}
48+
- {os: ubuntu-24.04, shell: bash}
49+
- {os: windows-latest, shell: bash}
50+
defaults:
51+
run:
52+
shell: ${{ matrix.sys.shell }}
53+
runs-on: ${{matrix.sys.os}}
5054
steps:
55+
# - uses: msys2/setup-msys2@v2
56+
# if: matrix.sys.os == 'windows-latest'
57+
# with:
58+
# update: true
59+
# install: >-
60+
# curl
61+
# git
62+
# pkg-config
63+
5164
- uses: actions/checkout@v4
5265

5366
- name: Set up Go
5467
uses: actions/setup-go@v4
5568
with:
5669
go-version: 1.23
5770

71+
- uses: actions/setup-python@v5
72+
with:
73+
python-version: '3.13'
74+
update-environment: true
75+
76+
- name: Generate Python pkg-config for windows (patch)
77+
if: matrix.sys.os == 'windows-latest'
78+
run: |
79+
mkdir -p $PKG_CONFIG_PATH
80+
cp .github/assets/python3-embed.pc $PKG_CONFIG_PATH/
81+
82+
- name: Install tiny-pkg-config for windows (patch)
83+
if: matrix.sys.os == 'windows-latest'
84+
run: |
85+
set -x
86+
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
87+
unzip /tmp/tiny-pkg-config.zip -d $HOME/bin
88+
mv $HOME/bin/tiny-pkg-config.exe $HOME/bin/pkg-config.exe
89+
echo $PKG_CONFIG_PATH
90+
cat $PKG_CONFIG_PATH/python3-embed.pc
91+
pkg-config --libs python3-embed
92+
pkg-config --cflags python3-embed
93+
5894
- name: Build
59-
run: go build -v ./...
95+
run: go install -v ./...
6096

6197
- name: Test with coverage
6298
run: go test -v -coverprofile=coverage.txt -covermode=atomic ./...
99+
100+
- name: Test gopy
101+
run: |
102+
gopy init $HOME/foo
103+
cd $HOME/foo
104+
gopy build -v .
105+
gopy run -v .
106+
gopy install -v .
63107
64108
- name: Upload coverage to Codecov
65-
if: matrix.os == 'ubuntu-24.04'
66109
uses: codecov/codecov-action@v4
67110
with:
68111
token: ${{ secrets.CODECOV_TOKEN }}

cmd/add.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
3+
*/
4+
package cmd
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
// addCmd represents the add command
13+
var addCmd = &cobra.Command{
14+
Use: "add",
15+
Short: "A brief description of your command",
16+
Long: `A longer description that spans multiple lines and likely contains examples
17+
and usage of using your command. For example:
18+
19+
Cobra is a CLI library for Go that empowers applications.
20+
This application is a tool to generate the needed files
21+
to quickly create a Cobra application.`,
22+
Run: func(cmd *cobra.Command, args []string) {
23+
fmt.Println("add called")
24+
},
25+
}
26+
27+
func init() {
28+
rootCmd.AddCommand(addCmd)
29+
30+
// Here you will define your flags and configuration settings.
31+
32+
// Cobra supports Persistent Flags which will work for this command
33+
// and all subcommands, e.g.:
34+
// addCmd.PersistentFlags().String("foo", "", "A help for foo")
35+
36+
// Cobra supports local flags which will only run when this command
37+
// is called directly, e.g.:
38+
// addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
39+
}

cmd/build.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
3+
*/
4+
package cmd
5+
6+
import (
7+
"fmt"
8+
"os"
9+
10+
"github.com/cpunion/go-python/cmd/internal/rungo"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
// buildCmd represents the build command
15+
var buildCmd = &cobra.Command{
16+
Use: "build [flags] [package]",
17+
Short: "Build a Go package with Python environment configured",
18+
Long: func() string {
19+
intro := "Build compiles a Go package with the Python environment properly configured.\n\n"
20+
help, err := rungo.GetGoCommandHelp("build")
21+
if err != nil {
22+
return intro + "Failed to get go help: " + err.Error()
23+
}
24+
return intro + help
25+
}(),
26+
DisableFlagParsing: true,
27+
Run: func(cmd *cobra.Command, args []string) {
28+
if err := rungo.RunGoCommand("build", args); err != nil {
29+
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
30+
os.Exit(1)
31+
}
32+
},
33+
}
34+
35+
func init() {
36+
rootCmd.AddCommand(buildCmd)
37+
}

cmd/gopy/gopy.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
3+
*/
4+
package main
5+
6+
import "github.com/cpunion/go-python/cmd"
7+
8+
func main() {
9+
cmd.Execute()
10+
}

cmd/init.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
3+
*/
4+
package cmd
5+
6+
import (
7+
"bufio"
8+
"fmt"
9+
"io"
10+
"os"
11+
"strings"
12+
13+
"github.com/cpunion/go-python/cmd/internal/create"
14+
"github.com/cpunion/go-python/cmd/internal/install"
15+
"github.com/fatih/color"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
var bold = color.New(color.Bold).SprintFunc()
20+
21+
// isDirEmpty checks if a directory is empty
22+
func isDirEmpty(path string) (bool, error) {
23+
f, err := os.Open(path)
24+
if err != nil {
25+
return false, err
26+
}
27+
defer f.Close()
28+
29+
_, err = f.Readdirnames(1)
30+
if err == io.EOF {
31+
return true, nil
32+
}
33+
return false, err
34+
}
35+
36+
// promptYesNo asks user for confirmation
37+
func promptYesNo(prompt string) bool {
38+
reader := bufio.NewReader(os.Stdin)
39+
fmt.Printf("%s [y/N]: ", prompt)
40+
response, err := reader.ReadString('\n')
41+
if err != nil {
42+
return false
43+
}
44+
45+
response = strings.ToLower(strings.TrimSpace(response))
46+
return response == "y" || response == "yes"
47+
}
48+
49+
// initCmd represents the init command
50+
var initCmd = &cobra.Command{
51+
Use: "init [path]",
52+
Short: "Initialize a new go-python project",
53+
Long: `Initialize a new go-python project in the specified directory.
54+
If no path is provided, it will initialize in the current directory.
55+
56+
Example:
57+
gopy init
58+
gopy init my-project
59+
gopy init --debug my-project
60+
gopy init -v my-project`,
61+
Run: func(cmd *cobra.Command, args []string) {
62+
// Get project path
63+
projectPath := "."
64+
if len(args) > 0 {
65+
projectPath = args[0]
66+
}
67+
68+
// Get flags
69+
debug, _ := cmd.Flags().GetBool("debug")
70+
verbose, _ := cmd.Flags().GetBool("verbose")
71+
goVersion, _ := cmd.Flags().GetString("go-version")
72+
pyVersion, _ := cmd.Flags().GetString("python-version")
73+
pyBuildDate, _ := cmd.Flags().GetString("python-build-date")
74+
pyFreeThreaded, _ := cmd.Flags().GetBool("python-free-threaded")
75+
tinyPkgConfigVersion, _ := cmd.Flags().GetString("tiny-pkg-config-version")
76+
77+
// Check if directory exists
78+
if _, err := os.Stat(projectPath); err == nil {
79+
// Directory exists, check if it's empty
80+
empty, err := isDirEmpty(projectPath)
81+
if err != nil {
82+
fmt.Printf("Error checking directory: %v\n", err)
83+
return
84+
}
85+
86+
if !empty {
87+
if !promptYesNo(fmt.Sprintf("Directory %s is not empty. Do you want to continue?", projectPath)) {
88+
fmt.Println("Operation cancelled")
89+
return
90+
}
91+
}
92+
} else if !os.IsNotExist(err) {
93+
fmt.Printf("Error checking directory: %v\n", err)
94+
return
95+
}
96+
97+
// Create project using the create package
98+
fmt.Printf("\n%s\n", bold("Creating project..."))
99+
if err := create.Project(projectPath, verbose); err != nil {
100+
fmt.Printf("Error creating project: %v\n", err)
101+
return
102+
}
103+
104+
// Install dependencies
105+
fmt.Printf("\n%s\n", bold("Installing dependencies..."))
106+
if err := install.Dependencies(projectPath, goVersion, tinyPkgConfigVersion, pyVersion, pyBuildDate, pyFreeThreaded, debug, verbose); err != nil {
107+
fmt.Printf("Error installing dependencies: %v\n", err)
108+
return
109+
}
110+
111+
fmt.Printf("\n%s\n", bold("Successfully initialized go-python project in "+projectPath))
112+
fmt.Println("\nNext steps:")
113+
fmt.Println("1. cd", projectPath)
114+
fmt.Println("2. gopy run .")
115+
},
116+
}
117+
118+
func init() {
119+
rootCmd.AddCommand(initCmd)
120+
initCmd.Flags().Bool("debug", false, "Install debug version of Python (not available on Windows)")
121+
initCmd.Flags().BoolP("verbose", "v", false, "Enable verbose output")
122+
initCmd.Flags().String("tiny-pkg-config-version", "v0.2.0", "tiny-pkg-config version to install")
123+
initCmd.Flags().String("go-version", "1.23.3", "Go version to install")
124+
initCmd.Flags().String("python-version", "3.13.0", "Python version to install")
125+
initCmd.Flags().String("python-build-date", "20241016", "Python build date")
126+
initCmd.Flags().Bool("python-free-threaded", false, "Install free-threaded version of Python")
127+
}

cmd/install.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
3+
*/
4+
package cmd
5+
6+
import (
7+
"fmt"
8+
"os"
9+
10+
"github.com/cpunion/go-python/cmd/internal/rungo"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
// installCmd represents the install command
15+
var installCmd = &cobra.Command{
16+
Use: "install [flags] [packages]",
17+
Short: "Install Go packages with Python environment configured",
18+
Long: func() string {
19+
intro := "Install compiles and installs Go packages with the Python environment properly configured.\n\n"
20+
help, err := rungo.GetGoCommandHelp("install")
21+
if err != nil {
22+
return intro + "Failed to get go help: " + err.Error()
23+
}
24+
return intro + help
25+
}(),
26+
DisableFlagParsing: true,
27+
Run: func(cmd *cobra.Command, args []string) {
28+
if err := rungo.RunGoCommand("install", args); err != nil {
29+
fmt.Println("Error:", err)
30+
os.Exit(1)
31+
}
32+
},
33+
}
34+
35+
func init() {
36+
rootCmd.AddCommand(installCmd)
37+
}

0 commit comments

Comments
 (0)