diff --git a/.golangci.yml b/.golangci.yml index 8965741..dadfbb7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -53,6 +53,9 @@ issues: - linters: - gochecknoglobals text: "Dotnet.* is a global variable" + - linters: + - gochecknoglobals + text: "Python.* is a global variable" - linters: - gochecknoglobals text: "Templates is a global variable" diff --git a/pkg/build/generate.go b/pkg/build/generate.go index a6125cd..6ad40fa 100644 --- a/pkg/build/generate.go +++ b/pkg/build/generate.go @@ -37,7 +37,7 @@ func generateDockerfile( } return dockerfile, buildContext, nil - } else if strings.HasSuffix(projectFile, "go.mod") { + } else if projectFile == "go.mod" { dockerfile, buildContext, err := generateDockerfileForGo( projectFile, applicationName, @@ -49,7 +49,7 @@ func generateDockerfile( } return dockerfile, buildContext, nil - } else if strings.HasSuffix(projectFile, "uv.lock") { + } else if projectFile == "pyproject.toml" { dockerfile, buildContext, err := generateDockerfileForPython( projectFile, directory, diff --git a/pkg/build/generate_python.go b/pkg/build/generate_python.go index d10f6dc..4d85435 100644 --- a/pkg/build/generate_python.go +++ b/pkg/build/generate_python.go @@ -12,6 +12,8 @@ import ( "github.com/3lvia/cli/pkg/utils" ) +const DefaultPythonVersion = "3.13" + type DockerfileVariablesPython struct { PythonVersion string } @@ -52,8 +54,6 @@ func generateDockerfileForPython( } func getPythonVersion(directories ...string) string { - const defaultPythonVersion = "3.13" - // removes duplicates slices.Sort(directories) directories = slices.Compact(directories) @@ -121,9 +121,9 @@ func getPythonVersion(directories ...string) string { style.PrintWarning( fmt.Sprintf( "Did not find any .python-version files, using default version %s.", - defaultPythonVersion, + DefaultPythonVersion, ), ) - return defaultPythonVersion + return DefaultPythonVersion } diff --git a/pkg/build/generate_test.go b/pkg/build/generate_test.go index b5617b5..3a260c1 100644 --- a/pkg/build/generate_test.go +++ b/pkg/build/generate_test.go @@ -308,7 +308,7 @@ func TestGeneratePythonDockerfile1(t *testing.T) { expectedBuildContext := "." const ( - projectFile = "uv.lock" + projectFile = "pyproject.toml" applicationName = "demo-api-python" ) @@ -345,7 +345,7 @@ func TestGeneratePythonDockerfile2(t *testing.T) { } tempDir := t.TempDir() - projectFile := filepath.Join(tempDir, "uv.lock") + projectFile := filepath.Join(tempDir, "pyproject.toml") expectedBuildContext := tempDir const applicationName = "demo-api-python" diff --git a/pkg/create/create.go b/pkg/create/create.go index d9f6dfe..37c24d6 100644 --- a/pkg/create/create.go +++ b/pkg/create/create.go @@ -8,6 +8,7 @@ import ( "path" "strings" + "github.com/3lvia/cli/pkg/build" "github.com/3lvia/cli/pkg/command" "github.com/3lvia/cli/pkg/githubactions" "github.com/3lvia/cli/pkg/shared" @@ -28,11 +29,13 @@ var ( // Dotnet8WebApp = Template{"dotnet8-webapp"}. Dotnet8Worker = Template{"dotnet8-worker"} // Go Template = Template{"go"}. + PythonAPI = Template{"python-api"} Templates = enum.New( Dotnet8WebAPI, // Dotnet8WebApp, Dotnet8Worker, // Go, + PythonAPI, ) ) @@ -83,6 +86,10 @@ var Command *cli.Command = &cli.Command{ Usage: "The root directory of your GitHub repository." + " The path specified will be prepended to '.github/workflows'.", }, + &cli.StringFlag{ + Name: "python-version", + Usage: "The version of Python to use for the project. Only applicable for Python templates.", + }, }, Action: Create, } @@ -118,6 +125,11 @@ func Create(ctx context.Context, c *cli.Command) error { defaultBranch := c.String("default-branch") nonInteractive := c.Bool("non-interactive") + pythonVersion := c.String("python-version") + + if template != PythonAPI && c.IsSet("python-version") { + style.PrintWarning("Argument 'python-version' is only applicable for Python templates.") + } checkCoooiecutterInstalledOutput := checkCookiecutterInstalledCommand(nil) if command.IsError(checkCoooiecutterInstalledOutput) { @@ -150,9 +162,9 @@ func Create(ctx context.Context, c *cli.Command) error { outputDirectory, applicationName, systemName, + pythonVersion, nil, ) - if command.IsError(cookiecutterOutput) { return cli.Exit("Failed to create project.", 1) } @@ -166,6 +178,13 @@ func Create(ctx context.Context, c *cli.Command) error { return cli.Exit(err, 1) } + if template == PythonAPI { + uvSyncOutput := uvSyncCommand(projectDirectory, nil) + if command.IsError(uvSyncOutput) { + return cli.Exit("Failed to generate uv.lock file.", 1) + } + } + githubActionsDirectory := func() string { if c.IsSet("github-actions-directory") { return c.String("github-actions-directory") @@ -214,10 +233,8 @@ func getProjectDirectoryForTemplate( outputDirectory, toPascalCaseWithoutHyphens(applicationName), ), nil - /* - case Go: - return path.Join(outputDirectory, applicationName), nil - */ + case PythonAPI /*, Go*/ : + return path.Join(outputDirectory, applicationName), nil default: return "", fmt.Errorf("Could not find project directory for template '%s'", template) } @@ -234,6 +251,8 @@ func getProjectFileForTemplate( case Go: return "go.mod", nil */ + case PythonAPI: + return "pyproject.toml", nil default: return "", fmt.Errorf("Could not find project file for template '%s'", template) } @@ -244,25 +263,31 @@ func cookiecutterCommand( outputDirectory string, applicationName string, systemName string, + pythonVersion string, options *command.RunOptions, ) command.Output { - return command.Run( - *exec.Command( - "cookiecutter", - "gh:3lvia/application-templates", - "--directory", - template.Value, - "--output-dir", - outputDirectory, - "--no-input", - "application_name="+applicationName, - "application_name_pascal_case="+toPascalCaseWithoutHyphens(applicationName), - "system_name="+systemName, - // TODO: is this needed? - "base_dir=./", - ), - options, + cmd := *exec.Command( + "cookiecutter", + "gh:3lvia/application-templates", + "--directory", + template.Value, + "--output-dir", + outputDirectory, + "--no-input", + "application_name="+applicationName, + "application_name_pascal_case="+toPascalCaseWithoutHyphens(applicationName), + "system_name="+systemName, ) + + if template == PythonAPI { + if pythonVersion == "" { + cmd.Args = append(cmd.Args, "python_version="+build.DefaultPythonVersion) + } else { + cmd.Args = append(cmd.Args, "python_version="+pythonVersion) + } + } + + return command.Run(cmd, options) } func checkCookiecutterInstalledCommand( @@ -303,3 +328,18 @@ func installCookiecutterCommand( options, ) } + +func uvSyncCommand( + projectDirectory string, + options *command.RunOptions, +) command.Output { + return command.Run( + *exec.Command( + "uv", + "sync", + "--directory", + projectDirectory, + ), + options, + ) +} diff --git a/pkg/githubactions/githubactions.go b/pkg/githubactions/githubactions.go index 3cfa6c7..f6ae9a7 100644 --- a/pkg/githubactions/githubactions.go +++ b/pkg/githubactions/githubactions.go @@ -324,6 +324,10 @@ func getLanguageFromProjectFile(projectFile string) (string, error) { return "go", nil } + if projectFile == "pyproject.toml" { + return "python", nil + } + if strings.Contains(projectFile, "Dockerfile") { return "dockerfile", nil } @@ -362,6 +366,23 @@ func getExampleWorkflowFileURL(language string, runtimeCloudProvider string) (st ) } + // Python + if language == "python" && runtimeCloudProvider == "aks" { + return exampleWorkflowBaseURL + "/build-deploy-python.yml", nil + } + + if language == "python" && runtimeCloudProvider == "gke" { + return exampleWorkflowBaseURL + "/build-deploy-python-google.yml", nil + } + + if language == "python" && runtimeCloudProvider == "iss" { + return "", + fmt.Errorf("Example workflow is not implemented yet for language '%s' and runtime cloud provider '%s'", + language, + runtimeCloudProvider, + ) + } + // Dockerfile if language == "dockerfile" && runtimeCloudProvider == "aks" { return exampleWorkflowBaseURL + "/build-deploy-dockerfile.yml", nil diff --git a/pkg/shared/flags.go b/pkg/shared/flags.go index 257f03d..e59160a 100644 --- a/pkg/shared/flags.go +++ b/pkg/shared/flags.go @@ -18,7 +18,7 @@ func ProjectFileFlag() *cli.StringFlag { Name: "project-file", Aliases: []string{"f"}, Usage: "The project file to use. We currently support .NET (*.csproj), Go (go.mod)," + - " Python with uv (uv.lock) or a generic Docker project (Dockerfile).", + " Python with uv (pyproject.toml) or a generic Docker project (Dockerfile).", } }