diff --git a/Makefile b/Makefile index 3b963f2..e8e828d 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,7 @@ cli-lint: cli-test: @echo "-- Running CLI tests" - cd cli && go test -coverprofile=c.out ./... + cd cli && gotestsum -f testname -- -coverprofile=c.out ./... cli-dep: @echo "-- Installing CLI dependencies" diff --git a/cli/cmd/delete/delete.go b/cli/cmd/delete/delete.go index 53be09a..2426cb3 100644 --- a/cli/cmd/delete/delete.go +++ b/cli/cmd/delete/delete.go @@ -6,9 +6,9 @@ import ( "os" "numerous/cli/cmd/output" + "numerous/cli/internal/dir" "numerous/cli/internal/gql" "numerous/cli/internal/gql/app" - "numerous/cli/tool" "git.sr.ht/~emersion/gqlclient" "github.com/spf13/cobra" @@ -31,7 +31,7 @@ func deleteApp(client *gqlclient.Client, args []string) error { var appID string if len(args) == 1 { appID = args[0] - } else if readAppID, err := tool.ReadAppIDAndPrintErrors("."); err != nil { + } else if readAppID, err := dir.ReadAppIDAndPrintErrors("."); err != nil { return err } else { appID = readAppID @@ -41,7 +41,7 @@ func deleteApp(client *gqlclient.Client, args []string) error { output.PrintError( "Sorry, we could not find the app in our database.", "Please, make sure that the App ID in the \"%s\" file is correct and try again.", - tool.AppIDFileName, + dir.AppIDFileName, ) return err diff --git a/cli/cmd/delete/delete_test.go b/cli/cmd/delete/delete_test.go index 6ba746e..6d8b78f 100644 --- a/cli/cmd/delete/delete_test.go +++ b/cli/cmd/delete/delete_test.go @@ -3,16 +3,16 @@ package deleteapp import ( "testing" + "numerous/cli/internal/dir" "numerous/cli/internal/gql/app" "numerous/cli/test" - "numerous/cli/tool" "github.com/stretchr/testify/assert" ) func TestAppDelete(t *testing.T) { t.Run("returns nil and successfully sends AppDelete mutations", func(t *testing.T) { - test.ChdirToTmpDirWithAppIDDocument(t, tool.AppIDFileName, "id") + test.ChdirToTmpDirWithAppIDDocument(t, dir.AppIDFileName, "id") response, _ := test.DeleteSuccessQueryResult() app := app.App{ ID: "id", @@ -26,7 +26,7 @@ func TestAppDelete(t *testing.T) { transportMock.AssertExpectations(t) }) t.Run("returns error if app does not exist", func(t *testing.T) { - test.ChdirToTmpDirWithAppIDDocument(t, tool.AppIDFileName, "id") + test.ChdirToTmpDirWithAppIDDocument(t, dir.AppIDFileName, "id") appNotFoundResponse := `"record not found"` response, _ := test.DeleteFailureQueryResult(appNotFoundResponse) c, transportMock := test.CreateMockGqlClient(response) diff --git a/cli/cmd/initialize/bootstrap.go b/cli/cmd/initialize/bootstrap.go index 6206b5f..ca73bb1 100644 --- a/cli/cmd/initialize/bootstrap.go +++ b/cli/cmd/initialize/bootstrap.go @@ -8,14 +8,14 @@ import ( "numerous/cli/assets" "numerous/cli/cmd/output" + "numerous/cli/internal/dir" "numerous/cli/manifest" - "numerous/cli/tool" ) const EnvFileName string = ".env" -func bootstrapFiles(t tool.Tool, toolID string, basePath string) error { - manifestToml, err := manifest.FromTool(t).ToToml() +func bootstrapFiles(m *manifest.Manifest, toolID string, basePath string) error { + manifestToml, err := m.ToToml() if err != nil { output.PrintErrorDetails("Error encoding manifest file", err) @@ -26,24 +26,24 @@ func bootstrapFiles(t tool.Tool, toolID string, basePath string) error { return err } - if err = addToGitIgnore(basePath, []string{"# added by numerous init\n", tool.AppIDFileName, EnvFileName}); err != nil { + if err = addToGitIgnore(basePath, []string{"# added by numerous init\n", dir.AppIDFileName, EnvFileName}); err != nil { return err } - appFilePath := filepath.Join(basePath, t.AppFile) - if err = createAndWriteIfFileNotExist(appFilePath, t.Library.DefaultAppFile()); err != nil { + appFilePath := filepath.Join(basePath, m.AppFile) + if err = createAndWriteIfFileNotExist(appFilePath, m.Library.DefaultAppFile()); err != nil { return err } - requirementsFilePath := filepath.Join(basePath, t.RequirementsFile) + requirementsFilePath := filepath.Join(basePath, m.RequirementsFile) if err = createFile(requirementsFilePath); err != nil { return err } - if err := bootstrapRequirements(t, basePath); err != nil { + if err := bootstrapRequirements(m, basePath); err != nil { return err } - if err = assets.CopyToolPlaceholderCover(filepath.Join(basePath, t.CoverImage)); err != nil { + if err = assets.CopyToolPlaceholderCover(filepath.Join(basePath, m.CoverImage)); err != nil { return err } @@ -54,8 +54,8 @@ func bootstrapFiles(t tool.Tool, toolID string, basePath string) error { return nil } -func bootstrapRequirements(t tool.Tool, basePath string) error { - requirementsPath := filepath.Join(basePath, t.RequirementsFile) +func bootstrapRequirements(m *manifest.Manifest, basePath string) error { + requirementsPath := filepath.Join(basePath, m.RequirementsFile) content, err := os.ReadFile(requirementsPath) if err != nil { return err @@ -67,7 +67,7 @@ func bootstrapRequirements(t tool.Tool, basePath string) error { } defer file.Close() - for _, requirement := range t.Library.Requirements { + for _, requirement := range m.Library.Requirements { if err := addRequirementToFile(file, content, requirement); err != nil { return err } diff --git a/cli/cmd/initialize/bootstrap_test.go b/cli/cmd/initialize/bootstrap_test.go index b7bb48a..c1cfe95 100644 --- a/cli/cmd/initialize/bootstrap_test.go +++ b/cli/cmd/initialize/bootstrap_test.go @@ -8,7 +8,6 @@ import ( "numerous/cli/manifest" "numerous/cli/test" - "numerous/cli/tool" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -42,9 +41,9 @@ func allFilesExist(fileNames []string) error { func TestBootstrapAllFiles(t *testing.T) { tempDir := t.TempDir() require.NoError(t, os.Chdir(tempDir)) - lib, err := tool.GetLibraryByKey("streamlit") + lib, err := manifest.GetLibraryByKey("streamlit") require.NoError(t, err) - testTool := tool.Tool{ + m := manifest.Manifest{ Library: lib, AppFile: "app.py", RequirementsFile: "requirements.txt", @@ -53,12 +52,12 @@ func TestBootstrapAllFiles(t *testing.T) { expectedFiles := []string{ ".gitignore", manifest.ManifestFileName, - testTool.AppFile, - testTool.RequirementsFile, - testTool.CoverImage, + m.AppFile, + m.RequirementsFile, + m.CoverImage, } - err = bootstrapFiles(testTool, "some-id", tempDir) + err = bootstrapFiles(&m, "some-id", tempDir) if assert.NoError(t, err) { err = allFilesExist(expectedFiles) @@ -74,49 +73,49 @@ func TestBootstrapRequirementsFile(t *testing.T) { testCases := []struct { name string - library tool.Library + library manifest.Library initialRequirements string expectedRequirements string }{ { name: "plotly-dash without initial requirements", - library: tool.LibraryPlotlyDash, + library: manifest.LibraryPlotlyDash, initialRequirements: "", expectedRequirements: "dash\ngunicorn\n", }, { name: "streamlit without initial requirements", - library: tool.LibraryStreamlit, + library: manifest.LibraryStreamlit, initialRequirements: "", expectedRequirements: "streamlit\n", }, { name: "marimo without initial requirements", - library: tool.LibraryMarimo, + library: manifest.LibraryMarimo, initialRequirements: "", expectedRequirements: "marimo\n", }, { name: "numerous without initial requirements", - library: tool.LibraryNumerous, + library: manifest.LibraryNumerous, initialRequirements: "", expectedRequirements: "numerous\n", }, { name: "marimo with initial requirements with newline appends at end", - library: tool.LibraryMarimo, + library: manifest.LibraryMarimo, initialRequirements: dummyRequirementsWithNewLine, expectedRequirements: dummyRequirementsWithNewLine + "marimo\n", }, { name: "marimo with initial requirements without newline appends at end", - library: tool.LibraryMarimo, + library: manifest.LibraryMarimo, initialRequirements: dummyRequirementsWithoutNewLine, expectedRequirements: dummyRequirementsWithNewLine + "marimo\n", }, { name: "marimo with initial requirements and library is part of requirements, nothing changes", - library: tool.LibraryMarimo, + library: manifest.LibraryMarimo, initialRequirements: "marimo\n" + dummyRequirementsWithNewLine, expectedRequirements: "marimo\n" + dummyRequirementsWithNewLine, }, @@ -125,21 +124,21 @@ func TestBootstrapRequirementsFile(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { tempDir := t.TempDir() require.NoError(t, os.Chdir(tempDir)) - testTool := tool.Tool{ + m := manifest.Manifest{ Library: testCase.library, AppFile: "app.py", RequirementsFile: "requirements.txt", CoverImage: "cover_image.png", } if testCase.initialRequirements != "" { - err := os.WriteFile(testTool.RequirementsFile, []byte(testCase.initialRequirements), 0o644) + err := os.WriteFile(m.RequirementsFile, []byte(testCase.initialRequirements), 0o644) require.NoError(t, err) } - err := bootstrapFiles(testTool, "some-id", tempDir) + err := bootstrapFiles(&m, "some-id", tempDir) require.NoError(t, err) - actualRequirements, err := os.ReadFile(testTool.RequirementsFile) + actualRequirements, err := os.ReadFile(m.RequirementsFile) require.NoError(t, err) assert.Equal(t, testCase.expectedRequirements, string(actualRequirements)) }) @@ -167,27 +166,27 @@ func TestBootstrapFiles(t *testing.T) { t.Run("bootstraps app file", func(t *testing.T) { testCases := []struct { name string - library tool.Library + library manifest.Library expectedAppFile string }{ { name: "numerous", - library: tool.LibraryNumerous, + library: manifest.LibraryNumerous, expectedAppFile: expectedNumerousApp, }, { name: "streamlit", - library: tool.LibraryStreamlit, + library: manifest.LibraryStreamlit, expectedAppFile: "", }, { name: "dash", - library: tool.LibraryPlotlyDash, + library: manifest.LibraryPlotlyDash, expectedAppFile: "", }, { name: "marimo", - library: tool.LibraryMarimo, + library: manifest.LibraryMarimo, expectedAppFile: "", }, } @@ -195,7 +194,7 @@ func TestBootstrapFiles(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { require.NoError(t, os.Chdir(t.TempDir())) - testTool := tool.Tool{ + m := manifest.Manifest{ Library: testCase.library, AppFile: "app.py", RequirementsFile: "requirements.txt", @@ -204,7 +203,7 @@ func TestBootstrapFiles(t *testing.T) { tempDir, err := os.Getwd() require.NoError(t, err) - err = bootstrapFiles(testTool, "tool id", tempDir) + err = bootstrapFiles(&m, "tool id", tempDir) require.NoError(t, err) appContent, err := os.ReadFile("app.py") @@ -216,7 +215,7 @@ func TestBootstrapFiles(t *testing.T) { t.Run("adds expected lines to existing .gitignore", func(t *testing.T) { tmpDir := t.TempDir() toolID := "tool-id" - tool := tool.Tool{ + m := manifest.Manifest{ RequirementsFile: "requirements.txt", AppFile: "app.py", CoverImage: "conver_img.png", @@ -226,7 +225,7 @@ func TestBootstrapFiles(t *testing.T) { gitignoreFilePath := filepath.Join(tmpDir, ".gitignore") test.WriteFile(t, gitignoreFilePath, []byte(initialGitIgnoreContent)) - err := bootstrapFiles(tool, toolID, tmpDir) + err := bootstrapFiles(&m, toolID, tmpDir) assert.NoError(t, err) actualGitIgnoreContent, err := os.ReadFile(gitignoreFilePath) @@ -238,18 +237,20 @@ func TestBootstrapFiles(t *testing.T) { t.Run("writes manifest with expected excludes", func(t *testing.T) { tmpDir := t.TempDir() toolID := "tool-id" - tool := tool.Tool{ + m := manifest.Manifest{ RequirementsFile: "requirements.txt", AppFile: "app.py", CoverImage: "conver_img.png", + Library: manifest.LibraryMarimo, + Exclude: []string{"*venv", "venv*", ".git", ".env"}, } - bootErr := bootstrapFiles(tool, toolID, tmpDir) - manifest, manifestErr := manifest.LoadManifest(tmpDir + "/" + manifest.ManifestFileName) + bootErr := bootstrapFiles(&m, toolID, tmpDir) + loaded, manifestErr := manifest.LoadManifest(tmpDir + "/" + manifest.ManifestFileName) assert.NoError(t, bootErr) assert.NoError(t, manifestErr) expectedExclude := []string{"*venv", "venv*", ".git", ".env"} - assert.Equal(t, expectedExclude, manifest.Exclude) + assert.Equal(t, expectedExclude, loaded.Exclude) }) } diff --git a/cli/cmd/initialize/files.go b/cli/cmd/initialize/files.go index 6e9d96e..c1dd1d5 100644 --- a/cli/cmd/initialize/files.go +++ b/cli/cmd/initialize/files.go @@ -8,7 +8,7 @@ import ( "strings" "numerous/cli/cmd/output" - "numerous/cli/tool" + "numerous/cli/internal/dir" ) // Creates file if it does not exists @@ -81,7 +81,7 @@ func writeOrAppendFile(path string, content string) error { // Generates and creates file containing the tools id func createAppIDFile(path string, id string) error { - appIDFile := filepath.Join(path, tool.AppIDFileName) + appIDFile := filepath.Join(path, dir.AppIDFileName) if err := createFile(appIDFile); err != nil { output.PrintUnknownError(err) return err diff --git a/cli/cmd/initialize/files_test.go b/cli/cmd/initialize/files_test.go index 2ee8db3..f29ff6c 100644 --- a/cli/cmd/initialize/files_test.go +++ b/cli/cmd/initialize/files_test.go @@ -5,8 +5,8 @@ import ( "path/filepath" "testing" + "numerous/cli/internal/dir" "numerous/cli/test" - "numerous/cli/tool" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,7 +29,7 @@ func TestCreateAppIdFile(t *testing.T) { tmpDir := t.TempDir() err := os.Chdir(tmpDir) require.NoError(t, err) - appIDPath := filepath.Join(tmpDir, tool.AppIDFileName) + appIDPath := filepath.Join(tmpDir, dir.AppIDFileName) appID := "some-id" err = createAppIDFile(tmpDir, appID) diff --git a/cli/cmd/initialize/get_python_version_test.go b/cli/cmd/initialize/get_python_version_test.go deleted file mode 100644 index e5b8c7e..0000000 --- a/cli/cmd/initialize/get_python_version_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package initialize - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestExtractPythonVersion(t *testing.T) { - tests := []struct { - version string - expectedExtractedVersion string - }{ - { - version: "3.12.4", - expectedExtractedVersion: "3.12", - }, - { - version: "3.7.0", - expectedExtractedVersion: "3.7", - }, - { - version: "2.7", - expectedExtractedVersion: "2.7", - }, - { - version: "1.5.1p1", - expectedExtractedVersion: "1.5", - }, - } - for _, test := range tests { - t.Run("Can extract python version "+test.expectedExtractedVersion, func(t *testing.T) { - actualExtractedVersion, err := extractPythonVersion([]byte("Python " + test.version)) - require.NoError(t, err) - assert.Equalf(t, test.expectedExtractedVersion, actualExtractedVersion, test.expectedExtractedVersion+"=="+actualExtractedVersion) - }) - } -} diff --git a/cli/cmd/initialize/init.go b/cli/cmd/initialize/init.go index bae98be..8754556 100644 --- a/cli/cmd/initialize/init.go +++ b/cli/cmd/initialize/init.go @@ -1,7 +1,6 @@ package initialize import ( - "errors" "fmt" "log/slog" "os" @@ -9,34 +8,32 @@ import ( "numerous/cli/cmd/initialize/wizard" "numerous/cli/cmd/output" + "numerous/cli/internal/dir" "numerous/cli/internal/gql" "numerous/cli/internal/gql/app" - "numerous/cli/tool" + "numerous/cli/internal/python" + "numerous/cli/manifest" "github.com/spf13/cobra" ) +var InitCmd = &cobra.Command{ + Use: "init [flags]", + Aliases: []string{"initialize"}, + Short: "Initialize a numerous project", + Long: `Helps the user bootstrap a python project as a numerous project.`, + Args: cobra.MaximumNArgs(1), + Run: runInit, +} + var ( - appLibraryString string - newApp = tool.Tool{CoverImage: "app_cover.jpg"} - InitCmd = &cobra.Command{ - Use: "init [flags]", - Aliases: []string{"initialize"}, - Short: "Initialize a numerous project", - Long: `Helps the user bootstrap a python project as a numerous project.`, - Args: cobra.MaximumNArgs(1), - Run: runInit, - } + name string + desc string + libraryKey string + appFile string + requirementsFile string ) -func setupFlags(a *tool.Tool) { - InitCmd.Flags().StringVarP(&a.Name, "name", "n", "", "Name of the app") - InitCmd.Flags().StringVarP(&a.Description, "description", "d", "", "Description of your app") - InitCmd.Flags().StringVarP(&appLibraryString, "app-library", "t", "", "Library the app is made with") - InitCmd.Flags().StringVarP(&a.AppFile, "app-file", "f", "", "Path to that main file of the project") - InitCmd.Flags().StringVarP(&a.RequirementsFile, "requirements-file", "r", "", "Requirements file of the project") -} - func runInit(cmd *cobra.Command, args []string) { projectFolderPath, err := os.Getwd() if err != nil { @@ -50,7 +47,7 @@ func runInit(cmd *cobra.Command, args []string) { projectFolderPath = pathArgumentHandler(args[0], projectFolderPath) } - if exist, _ := tool.AppIDExistsInCurrentDir(projectFolderPath); exist { + if exist, _ := dir.AppIDExists(projectFolderPath); exist { output.PrintError( "An app is already initialized in \"%s\"", "💡 You can initialize an app in another folder by specifying a\n"+ @@ -62,14 +59,16 @@ func runInit(cmd *cobra.Command, args []string) { return } - if err := validateAndSetAppLibrary(&newApp, appLibraryString); err != nil { - fmt.Println(err) - return + lib, err := manifest.GetLibraryByKey(libraryKey) + if libraryKey != "" && err != nil { + output.PrintErrorDetails("Unsupported library", err) + os.Exit(1) } - setPython(&newApp) + pythonVersion := python.PythonVersion() - if continueBootstrap, err := wizard.RunInitAppWizard(projectFolderPath, &newApp); err != nil { + m := manifest.New(lib, name, desc, pythonVersion, appFile, requirementsFile) + if continueBootstrap, err := wizard.RunInitAppWizard(projectFolderPath, m); err != nil { output.PrintErrorDetails("Error running initialization wizard", err) return } else if !continueBootstrap { @@ -77,13 +76,13 @@ func runInit(cmd *cobra.Command, args []string) { } // Initialize and boostrap project files - a, err := app.Create(newApp, gql.GetClient()) + a, err := app.Create(m, gql.GetClient()) if err != nil { output.PrintErrorDetails("Error registering app remotely.", err) return } - if err := bootstrapFiles(newApp, a.ID, projectFolderPath); err != nil { + if err := bootstrapFiles(m, a.ID, projectFolderPath); err != nil { output.PrintErrorDetails("Error bootstrapping files.", err) return } @@ -107,33 +106,6 @@ func pathArgumentHandler(providedPath string, currentPath string) string { return appPath } -func validateAndSetAppLibrary(a *tool.Tool, l string) error { - if l == "" { - return nil - } - lib, err := tool.GetLibraryByKey(l) - if err != nil { - return err - } - a.Library = lib - - return nil -} - -func setPython(a *tool.Tool) { - fallbackVersion := "3.11" - - if version, err := getPythonVersion(); errors.Is(err, ErrDetectPythonExecutable) { - fmt.Printf("Python interpeter not found, setting Python version to '%s' for the app.\n", fallbackVersion) - a.Python = fallbackVersion - } else if errors.Is(err, ErrDetectPythonVersion) { - fmt.Printf("Could not parse python version '%s', setting Python version to '%s' for the app.\n", version, fallbackVersion) - a.Python = fallbackVersion - } else { - a.Python = version - } -} - func printSuccess(a app.App) { fmt.Printf(` The app has been initialized! 🎉 @@ -144,9 +116,13 @@ The App ID %q is stored in %q and is used to identify the app in commands which If %q is removed, the CLI cannot identify your app. If you are logged in, you can use numerous list to find the App ID again. -`, a.ID, tool.AppIDFileName, tool.AppIDFileName) +`, a.ID, dir.AppIDFileName, dir.AppIDFileName) } func init() { - setupFlags(&newApp) + InitCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the app") + InitCmd.Flags().StringVarP(&desc, "description", "d", "", "Description of your app") + InitCmd.Flags().StringVarP(&libraryKey, "app-library", "t", "", "Library the app is made with") + InitCmd.Flags().StringVarP(&appFile, "app-file", "f", "", "Path to that main file of the project") + InitCmd.Flags().StringVarP(&requirementsFile, "requirements-file", "r", "", "Requirements file of the project") } diff --git a/cli/cmd/initialize/init_test.go b/cli/cmd/initialize/init_test.go deleted file mode 100644 index 9728702..0000000 --- a/cli/cmd/initialize/init_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package initialize - -import ( - "testing" - - "numerous/cli/tool" - - "github.com/stretchr/testify/assert" -) - -func TestValidateFlags(t *testing.T) { - t.Run("Validates if no library is set", func(t *testing.T) { - err := validateAndSetAppLibrary(&tool.Tool{}, "") - assert.NoError(t, err) - }) - - t.Run("Cannot validate unsupported library", func(t *testing.T) { - err := validateAndSetAppLibrary(&tool.Tool{}, "something") - assert.Error(t, err) - }) - - for _, lib := range []string{"plotly", "marimo", "streamlit"} { - t.Run("Validates "+lib, func(t *testing.T) { - err := validateAndSetAppLibrary(&tool.Tool{}, lib) - assert.NoError(t, err) - }) - } -} diff --git a/cli/cmd/initialize/wizard/library_question.go b/cli/cmd/initialize/wizard/library_question.go index 7e21952..8ae428d 100644 --- a/cli/cmd/initialize/wizard/library_question.go +++ b/cli/cmd/initialize/wizard/library_question.go @@ -1,14 +1,14 @@ package wizard import ( - "numerous/cli/tool" + "numerous/cli/manifest" "github.com/AlecAivazis/survey/v2" ) func getLibraryQuestion(name, prompt string) *survey.Question { libraryNames := []string{} - for _, lib := range tool.SupportedLibraries { + for _, lib := range manifest.SupportedLibraries { libraryNames = append(libraryNames, lib.Name) } diff --git a/cli/cmd/initialize/wizard/survey_answers.go b/cli/cmd/initialize/wizard/survey_answers.go index 6e60987..fac14e3 100644 --- a/cli/cmd/initialize/wizard/survey_answers.go +++ b/cli/cmd/initialize/wizard/survey_answers.go @@ -3,7 +3,7 @@ package wizard import ( "strings" - "numerous/cli/tool" + "numerous/cli/manifest" ) type surveyAnswers struct { @@ -14,20 +14,22 @@ type surveyAnswers struct { RequirementsFile string } -func (s surveyAnswers) appendAnswersToApp(a *tool.Tool) { - a.Name = s.Name - a.Description = s.Description - a.Library, _ = tool.GetLibraryByName(s.LibraryName) - a.AppFile = strings.Trim(s.AppFile, " ") - a.RequirementsFile = strings.Trim(s.RequirementsFile, " ") +func (s surveyAnswers) updateManifest(m *manifest.Manifest) { + lib, _ := manifest.GetLibraryByName(s.LibraryName) + m.Name = s.Name + m.Description = s.Description + m.Library = lib + m.AppFile = strings.Trim(s.AppFile, " ") + m.RequirementsFile = strings.Trim(s.RequirementsFile, " ") + m.Port = lib.Port } -func fromApp(a *tool.Tool) *surveyAnswers { - return &surveyAnswers{ - Name: a.Name, - Description: a.Description, - LibraryName: a.Library.Name, - AppFile: a.AppFile, - RequirementsFile: a.RequirementsFile, +func answersFromManifest(m *manifest.Manifest) surveyAnswers { + return surveyAnswers{ + Name: m.Name, + Description: m.Description, + LibraryName: m.Library.Name, + AppFile: m.AppFile, + RequirementsFile: m.RequirementsFile, } } diff --git a/cli/cmd/initialize/wizard/wizard.go b/cli/cmd/initialize/wizard/wizard.go index 191d98b..e9fb42b 100644 --- a/cli/cmd/initialize/wizard/wizard.go +++ b/cli/cmd/initialize/wizard/wizard.go @@ -4,13 +4,21 @@ import ( "fmt" "os" - "numerous/cli/tool" + "numerous/cli/manifest" "github.com/AlecAivazis/survey/v2" ) -func RunInitAppWizard(projectFolderPath string, a *tool.Tool) (bool, error) { - questions := getQuestions(*a) +type InitWizardOptions struct { + Name string + Description string + LibraryKey string + AppFile string + RequirementsFile string +} + +func RunInitAppWizard(projectFolderPath string, m *manifest.Manifest) (bool, error) { + questions := getQuestions(m) if len(questions) == 1 && questions[0].Name == "Description" { return false, nil } @@ -26,42 +34,43 @@ func RunInitAppWizard(projectFolderPath string, a *tool.Tool) (bool, error) { return false, nil } - answers := fromApp(a) - if err := survey.Ask(questions, answers); err != nil { + answers := answersFromManifest(m) + if err := survey.Ask(questions, &answers); err != nil { return false, err } - answers.appendAnswersToApp(a) + answers.updateManifest(m) return true, nil } -func getQuestions(a tool.Tool) []*survey.Question { - q := []*survey.Question{} +func getQuestions(m *manifest.Manifest) []*survey.Question { + qs := []*survey.Question{} - if a.Name == "" { - q = append(q, getTextQuestion("Name", - "Name your app:", - true)) + if m.Name == "" { + q := getTextQuestion("Name", "Name your app:", true) + qs = append(qs, q) } - if a.Description == "" { - q = append(q, getTextQuestion("Description", - "Provide a short description for your app:", - false)) + + if m.Description == "" { + q := getTextQuestion("Description", "Provide a short description for your app:", false) + qs = append(qs, q) } - if a.Library.Key == "" { - q = append(q, getLibraryQuestion("LibraryName", - "Select which app library you are using:")) + + if m.Library.Key == "" { + q := getLibraryQuestion("LibraryName", "Select which app library you are using:") + qs = append(qs, q) } - if a.AppFile == "" { - q = append(q, getFileQuestion("AppFile", + + if m.AppFile == "" { + qs = append(qs, getFileQuestion("AppFile", "Provide the path to your app:", "app.py", ".py")) } - if a.RequirementsFile == "" { - q = append(q, getFileQuestion("RequirementsFile", - "Provide the path to your requirements file:", - "requirements.txt", ".txt")) + + if m.RequirementsFile == "" { + q := getFileQuestion("RequirementsFile", "Provide the path to your requirements file:", "requirements.txt", ".txt") + qs = append(qs, q) } - return q + return qs } diff --git a/cli/cmd/initialize/wizard/wizard_test.go b/cli/cmd/initialize/wizard/wizard_test.go new file mode 100644 index 0000000..f6df1c9 --- /dev/null +++ b/cli/cmd/initialize/wizard/wizard_test.go @@ -0,0 +1,29 @@ +package wizard + +import ( + "testing" + + "numerous/cli/manifest" + + "github.com/stretchr/testify/assert" +) + +func TestGetQuestions(t *testing.T) { + t.Run("given empty manifest gets all questions", func(t *testing.T) { + qs := getQuestions(&manifest.Manifest{}) + + assert.Len(t, qs, 5) + }) + + t.Run("given full manifest gets no questions", func(t *testing.T) { + qs := getQuestions(&manifest.Manifest{ + Name: "Some name", + Description: "Some description", + Library: manifest.LibraryNumerous, + AppFile: "app.py", + RequirementsFile: "requirements.txt", + }) + + assert.Empty(t, qs) + }) +} diff --git a/cli/cmd/log/log.go b/cli/cmd/log/log.go index 3a0eecf..89d2754 100644 --- a/cli/cmd/log/log.go +++ b/cli/cmd/log/log.go @@ -4,7 +4,7 @@ import ( "os" "numerous/cli/cmd/output" - "numerous/cli/tool" + "numerous/cli/internal/dir" "github.com/spf13/cobra" ) @@ -35,7 +35,7 @@ func log(cmd *cobra.Command, args []string) { return } - appID, err := tool.ReadAppIDAndPrintErrors(appDir) + appID, err := dir.ReadAppIDAndPrintErrors(appDir) if err != nil { return } diff --git a/cli/cmd/publish/publish.go b/cli/cmd/publish/publish.go index 2d6b143..129fc4b 100644 --- a/cli/cmd/publish/publish.go +++ b/cli/cmd/publish/publish.go @@ -4,9 +4,9 @@ import ( "fmt" "os" + "numerous/cli/internal/dir" "numerous/cli/internal/gql" "numerous/cli/internal/gql/app" - "numerous/cli/tool" "git.sr.ht/~emersion/gqlclient" "github.com/spf13/cobra" @@ -24,7 +24,7 @@ var PublishCmd = &cobra.Command{ } func publish(client *gqlclient.Client) error { - appID, err := tool.ReadAppIDAndPrintErrors(".") + appID, err := dir.ReadAppIDAndPrintErrors(".") if err != nil { return err } diff --git a/cli/cmd/publish/publish_test.go b/cli/cmd/publish/publish_test.go index 18f0137..3f5540a 100644 --- a/cli/cmd/publish/publish_test.go +++ b/cli/cmd/publish/publish_test.go @@ -3,16 +3,16 @@ package publish import ( "testing" + "numerous/cli/internal/dir" "numerous/cli/internal/gql/app" "numerous/cli/test" - "numerous/cli/tool" "github.com/stretchr/testify/assert" ) func TestAppPublish(t *testing.T) { t.Run("returns nil and successfully sends AppPublish mutations", func(t *testing.T) { - test.ChdirToTmpDirWithAppIDDocument(t, tool.AppIDFileName, "id") + test.ChdirToTmpDirWithAppIDDocument(t, dir.AppIDFileName, "id") app := app.App{ ID: "id", SharedURL: "https://test.com/shared/some-hash", @@ -28,7 +28,7 @@ func TestAppPublish(t *testing.T) { }) t.Run("returns error if app does not exist", func(t *testing.T) { - test.ChdirToTmpDirWithAppIDDocument(t, tool.AppIDFileName, "id") + test.ChdirToTmpDirWithAppIDDocument(t, dir.AppIDFileName, "id") appNotFoundResponse := `{"errors":[{"message":"record not found","path":["tool"]}],"data":null}` c, transportMock := test.CreateMockGqlClient(appNotFoundResponse) @@ -47,7 +47,7 @@ func TestAppPublish(t *testing.T) { }) t.Run("return nil and does not send AppPublish mutation, if app is published", func(t *testing.T) { - test.ChdirToTmpDirWithAppIDDocument(t, tool.AppIDFileName, "id") + test.ChdirToTmpDirWithAppIDDocument(t, dir.AppIDFileName, "id") app := app.App{ ID: "id", SharedURL: "https://test.com/shared/some-hash", diff --git a/cli/cmd/push/push.go b/cli/cmd/push/push.go index 97c3c0c..48eab01 100644 --- a/cli/cmd/push/push.go +++ b/cli/cmd/push/push.go @@ -11,11 +11,11 @@ import ( "numerous/cli/cmd/output" "numerous/cli/dotenv" "numerous/cli/internal/archive" + "numerous/cli/internal/dir" "numerous/cli/internal/gql" "numerous/cli/internal/gql/app" "numerous/cli/internal/gql/build" "numerous/cli/manifest" - "numerous/cli/tool" "github.com/spf13/cobra" ) @@ -47,7 +47,7 @@ func push(cmd *cobra.Command, args []string) { os.Exit(1) } - toolID, err := tool.ReadAppIDAndPrintErrors(appDir) + toolID, err := dir.ReadAppIDAndPrintErrors(appDir) if err != nil { os.Exit(1) } diff --git a/cli/cmd/unpublish/unpublish.go b/cli/cmd/unpublish/unpublish.go index d96a4d4..1662798 100644 --- a/cli/cmd/unpublish/unpublish.go +++ b/cli/cmd/unpublish/unpublish.go @@ -5,9 +5,9 @@ import ( "os" "numerous/cli/cmd/output" + "numerous/cli/internal/dir" "numerous/cli/internal/gql" "numerous/cli/internal/gql/app" - "numerous/cli/tool" "git.sr.ht/~emersion/gqlclient" "github.com/spf13/cobra" @@ -25,7 +25,7 @@ var UnpublishCmd = &cobra.Command{ } func unpublish(client *gqlclient.Client) error { - appID, err := tool.ReadAppIDAndPrintErrors(".") + appID, err := dir.ReadAppIDAndPrintErrors(".") if err != nil { return err } diff --git a/cli/cmd/unpublish/unpublish_test.go b/cli/cmd/unpublish/unpublish_test.go index f5b8328..81bf5ef 100644 --- a/cli/cmd/unpublish/unpublish_test.go +++ b/cli/cmd/unpublish/unpublish_test.go @@ -3,16 +3,16 @@ package unpublish import ( "testing" + "numerous/cli/internal/dir" "numerous/cli/internal/gql/app" "numerous/cli/test" - "numerous/cli/tool" "github.com/stretchr/testify/assert" ) func TestAppPublish(t *testing.T) { t.Run("returns nil and successfully sends AppUnpublish mutations", func(t *testing.T) { - test.ChdirToTmpDirWithAppIDDocument(t, tool.AppIDFileName, "id") + test.ChdirToTmpDirWithAppIDDocument(t, dir.AppIDFileName, "id") app := app.App{ ID: "id", SharedURL: "https://test.com/shared/some-hash", @@ -30,7 +30,7 @@ func TestAppPublish(t *testing.T) { }) t.Run("returns error if app does not exist", func(t *testing.T) { - test.ChdirToTmpDirWithAppIDDocument(t, tool.AppIDFileName, "id") + test.ChdirToTmpDirWithAppIDDocument(t, dir.AppIDFileName, "id") appNotFoundResponse := `{"errors":[{"message":"record not found","path":["tool"]}],"data":null}` c, transportMock := test.CreateMockGqlClient(appNotFoundResponse) @@ -49,7 +49,7 @@ func TestAppPublish(t *testing.T) { }) t.Run("return nil and does not send AppUnpublish mutation, if app is not published", func(t *testing.T) { - test.ChdirToTmpDirWithAppIDDocument(t, tool.AppIDFileName, "id") + test.ChdirToTmpDirWithAppIDDocument(t, dir.AppIDFileName, "id") app := app.App{ ID: "id", SharedURL: "https://test.com/shared/some-hash", diff --git a/cli/tool/tool.go b/cli/internal/dir/appid.go similarity index 66% rename from cli/tool/tool.go rename to cli/internal/dir/appid.go index b2732aa..63b83e4 100644 --- a/cli/tool/tool.go +++ b/cli/internal/dir/appid.go @@ -1,8 +1,7 @@ -package tool +package dir import ( "errors" - "fmt" "os" "path/filepath" @@ -16,18 +15,8 @@ const ( var ErrAppIDNotFound = errors.New("app id not found") -type Tool struct { - Name string - Description string - Library Library - Python string - AppFile string - RequirementsFile string - CoverImage string -} - -func AppIDExistsInCurrentDir(basePath string) (bool, error) { - appIDFilePath := filepath.Join(basePath, AppIDFileName) +func AppIDExists(dir string) (bool, error) { + appIDFilePath := filepath.Join(dir, AppIDFileName) _, err := os.Stat(appIDFilePath) if err == nil { return true, nil @@ -35,7 +24,7 @@ func AppIDExistsInCurrentDir(basePath string) (bool, error) { return true, err } - toolIDFilePath := filepath.Join(basePath, ToolIDFileName) + toolIDFilePath := filepath.Join(dir, ToolIDFileName) _, err = os.Stat(toolIDFilePath) if err == nil { return true, nil @@ -79,16 +68,3 @@ func ReadAppIDAndPrintErrors(appDir string) (string, error) { return appID, nil } - -func (t Tool) String() string { - return fmt.Sprintf(` -Tool: - name %s - description %s - library %s - python %s - appFile %s - requirementsFile %s - coverImage %s - `, t.Name, t.Description, t.Library.Key, t.Python, t.AppFile, t.RequirementsFile, t.CoverImage) -} diff --git a/cli/tool/tool_test.go b/cli/internal/dir/appid_test.go similarity index 81% rename from cli/tool/tool_test.go rename to cli/internal/dir/appid_test.go index 7d1938f..2140cca 100644 --- a/cli/tool/tool_test.go +++ b/cli/internal/dir/appid_test.go @@ -1,8 +1,7 @@ -package tool +package dir import ( "fmt" - "os" "path/filepath" "testing" @@ -53,45 +52,35 @@ func TestReadAppID(t *testing.T) { }) } -func TestAppIDExistsInCurrentDir(t *testing.T) { +func TestAppIDExists(t *testing.T) { someAppID := "app-id-goes-here" t.Run(AppIDFileName+" exists", func(t *testing.T) { - tmpDir := createTempDirAndChdir(t) + tmpDir := t.TempDir() test.WriteFile(t, filepath.Join(tmpDir, AppIDFileName), []byte(someAppID)) - exists, err := AppIDExistsInCurrentDir(tmpDir) + exists, err := AppIDExists(tmpDir) assert.NoError(t, err) assert.True(t, exists) }) t.Run(ToolIDFileName+" exists", func(t *testing.T) { - tmpDir := createTempDirAndChdir(t) + tmpDir := t.TempDir() test.WriteFile(t, filepath.Join(tmpDir, ToolIDFileName), []byte(someAppID)) - exists, err := AppIDExistsInCurrentDir(tmpDir) + exists, err := AppIDExists(tmpDir) assert.NoError(t, err) assert.True(t, exists) }) t.Run("no app ID file exists", func(t *testing.T) { - tmpDir := createTempDirAndChdir(t) + tmpDir := t.TempDir() - exists, err := AppIDExistsInCurrentDir(tmpDir) + exists, err := AppIDExists(tmpDir) assert.NoError(t, err) assert.False(t, exists) }) } - -func createTempDirAndChdir(t *testing.T) string { - t.Helper() - - tmpDir := t.TempDir() - err := os.Chdir(tmpDir) - require.NoError(t, err) - - return tmpDir -} diff --git a/cli/internal/gql/app/create.go b/cli/internal/gql/app/create.go index 14d76e7..40bdc5e 100644 --- a/cli/internal/gql/app/create.go +++ b/cli/internal/gql/app/create.go @@ -4,7 +4,6 @@ import ( "context" "numerous/cli/manifest" - "numerous/cli/tool" "git.sr.ht/~emersion/gqlclient" ) @@ -13,9 +12,9 @@ type appCreateResponse struct { ToolCreate App } -func Create(a tool.Tool, client *gqlclient.Client) (App, error) { +func Create(m *manifest.Manifest, client *gqlclient.Client) (App, error) { resp := appCreateResponse{} - jsonManifest, err := manifest.FromTool(a).ToJSON() + jsonManifest, err := m.ToJSON() if err != nil { return resp.ToolCreate, err } diff --git a/cli/internal/gql/app/create_test.go b/cli/internal/gql/app/create_test.go index e4a8afa..1b49caf 100644 --- a/cli/internal/gql/app/create_test.go +++ b/cli/internal/gql/app/create_test.go @@ -4,16 +4,16 @@ import ( "testing" "time" + "numerous/cli/manifest" "numerous/cli/test" - "numerous/cli/tool" "github.com/stretchr/testify/assert" ) func TestCreate(t *testing.T) { - testApp := tool.Tool{ + m := &manifest.Manifest{ Name: "name", - Library: tool.LibraryMarimo, + Library: manifest.LibraryMarimo, Python: "3.11", AppFile: "app.py", RequirementsFile: "requirements.txt", @@ -30,7 +30,7 @@ func TestCreate(t *testing.T) { response := test.AppToQueryResult("toolCreate", expectedApp) c := test.CreateTestGqlClient(t, response) - actualApp, err := Create(testApp, c) + actualApp, err := Create(m, c) assert.NoError(t, err) assert.Equal(t, expectedApp, actualApp) @@ -40,7 +40,7 @@ func TestCreate(t *testing.T) { appNotFoundResponse := `{"errors":[{"message":"Something went wrong","path":["toolCreate"]}],"data":null}` c := test.CreateTestGqlClient(t, appNotFoundResponse) - actualApp, err := Create(testApp, c) + actualApp, err := Create(m, c) assert.Error(t, err) assert.ErrorContains(t, err, "Something went wrong") diff --git a/cli/cmd/initialize/get_python_version.go b/cli/internal/python/version.go similarity index 63% rename from cli/cmd/initialize/get_python_version.go rename to cli/internal/python/version.go index fc91774..616bcd4 100644 --- a/cli/cmd/initialize/get_python_version.go +++ b/cli/internal/python/version.go @@ -1,7 +1,8 @@ -package initialize +package python import ( "errors" + "fmt" "os/exec" "regexp" ) @@ -11,6 +12,28 @@ var ( ErrDetectPythonVersion = errors.New("could not detect python version") ) +// Returns the python version used in the environment, or 3.11 as a fallback if +// it cannot be detected. +func PythonVersion() string { + fallbackVersion := "3.11" + + version, err := getPythonVersion() + + if err == nil { + return version + } + + if errors.Is(err, ErrDetectPythonExecutable) { + fmt.Printf("Python interpeter not found, setting Python version to '%s' for the app.\n", fallbackVersion) + } + + if errors.Is(err, ErrDetectPythonVersion) { + fmt.Printf("Could not parse python version '%s', setting Python version to '%s' for the app.\n", version, fallbackVersion) + } + + return fallbackVersion +} + func getPythonVersion() (string, error) { p, err := execPythonVersionCommand() if err != nil { diff --git a/cli/tool/library.go b/cli/manifest/library.go similarity index 85% rename from cli/tool/library.go rename to cli/manifest/library.go index 6f05fb0..f5706d9 100644 --- a/cli/tool/library.go +++ b/cli/manifest/library.go @@ -1,9 +1,23 @@ -package tool +package manifest import ( "fmt" ) +var ( + streamlitPort uint = 80 + plotyPort uint = 8050 + marimoPort uint = 8000 + numerousPort uint = 7001 +) + +var ( + LibraryStreamlit = Library{Name: "Streamlit", Key: "streamlit", Port: streamlitPort, Requirements: []string{"streamlit"}} + LibraryPlotlyDash = Library{Name: "Plotly-dash", Key: "plotly", Port: plotyPort, Requirements: []string{"dash", "gunicorn"}} + LibraryMarimo = Library{Name: "Marimo", Key: "marimo", Port: marimoPort, Requirements: []string{"marimo"}} + LibraryNumerous = Library{Name: "Numerous", Key: "numerous", Port: numerousPort, Requirements: []string{"numerous"}} +) + type Library struct { Name string Key string @@ -28,6 +42,24 @@ class MyApp: appdef = MyApp ` +func (l *Library) MarshalText() ([]byte, error) { + return []byte(l.Key), nil +} + +func (l *Library) UnmarshalText(text []byte) error { + parsed, err := GetLibraryByKey(string(text)) + if err != nil { + return err + } + + l.Key = parsed.Key + l.Name = parsed.Name + l.Port = parsed.Port + l.Requirements = parsed.Requirements + + return nil +} + func (l *Library) DefaultAppFile() string { switch l.Key { case "numerous": @@ -37,19 +69,6 @@ func (l *Library) DefaultAppFile() string { } } -var ( - streamlitPort uint = 80 - plotyPort uint = 8050 - marimoPort uint = 8000 - numerousPort uint = 7001 -) - -var ( - LibraryStreamlit = Library{Name: "Streamlit", Key: "streamlit", Port: streamlitPort, Requirements: []string{"streamlit"}} - LibraryPlotlyDash = Library{Name: "Plotly-dash", Key: "plotly", Port: plotyPort, Requirements: []string{"dash", "gunicorn"}} - LibraryMarimo = Library{Name: "Marimo", Key: "marimo", Port: marimoPort, Requirements: []string{"marimo"}} - LibraryNumerous = Library{Name: "Numerous", Key: "numerous", Port: numerousPort, Requirements: []string{"numerous"}} -) var SupportedLibraries = []Library{LibraryStreamlit, LibraryPlotlyDash, LibraryMarimo, LibraryNumerous} func GetLibraryByKey(key string) (Library, error) { diff --git a/cli/tool/library_test.go b/cli/manifest/library_test.go similarity index 83% rename from cli/tool/library_test.go rename to cli/manifest/library_test.go index 3f3618c..f220a20 100644 --- a/cli/tool/library_test.go +++ b/cli/manifest/library_test.go @@ -1,8 +1,9 @@ -package tool +package manifest import ( "testing" + "github.com/BurntSushi/toml" "github.com/stretchr/testify/assert" ) @@ -81,3 +82,22 @@ func TestDefaultAppFile(t *testing.T) { assert.Equal(t, testCase.expected, actual) } } + +func TestUnmarshalLibrary(t *testing.T) { + testCases := SupportedLibraries + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + type Container struct { + Library Library + } + + var c Container + data := []byte("library = \"" + tc.Key + "\"") + err := toml.Unmarshal(data, &c) + + assert.NoError(t, err) + assert.Equal(t, Container{Library: tc}, c) + }) + } +} diff --git a/cli/manifest/manifest.go b/cli/manifest/manifest.go index 6ecffcc..c740c21 100644 --- a/cli/manifest/manifest.go +++ b/cli/manifest/manifest.go @@ -7,10 +7,6 @@ import ( "os" "path/filepath" "strconv" - "strings" - - "numerous/cli/cmd/output" - "numerous/cli/tool" "github.com/BurntSushi/toml" ) @@ -22,7 +18,7 @@ var ManifestPath string = filepath.Join(".", ManifestFileName) type Manifest struct { Name string `toml:"name" json:"name"` Description string `toml:"description" json:"description"` - Library string `toml:"library" json:"library"` + Library Library `toml:"library" json:"library"` Python string `toml:"python" json:"python"` AppFile string `toml:"app_file" json:"app_file"` RequirementsFile string `toml:"requirements_file" json:"requirements_file"` @@ -34,7 +30,7 @@ type Manifest struct { type DeprecatedManifest struct { Name string `toml:"name" json:"name"` Description string `toml:"description" json:"description"` - Library string `toml:"library" json:"library"` + Library Library `toml:"library" json:"library"` Python string `toml:"python" json:"python"` AppFile string `toml:"app_file" json:"app_file"` RequirementsFile string `toml:"requirements_file" json:"requirements_file"` @@ -85,16 +81,16 @@ func (d *DeprecatedManifest) ToManifest() (*Manifest, error) { return &m, nil } -func FromTool(t tool.Tool) *Manifest { +func New(lib Library, name string, description string, python string, appFile string, requirementsFile string) *Manifest { return &Manifest{ - Name: t.Name, - Description: t.Description, - Library: t.Library.Key, - Python: t.Python, - AppFile: t.AppFile, - RequirementsFile: t.RequirementsFile, - Port: t.Library.Port, - CoverImage: t.CoverImage, + Name: name, + Description: description, + Library: lib, + Python: python, + AppFile: appFile, + RequirementsFile: requirementsFile, + Port: lib.Port, + CoverImage: "app_cover.jpg", Exclude: []string{"*venv", "venv*", ".git", ".env"}, } } @@ -118,41 +114,3 @@ func (m *Manifest) ToJSON() (string, error) { return string(manifest), err } - -// Validates that the app defined in the manifest is valid. Returns false, if -// the app is in a state, where it does not make sense, to be able to push the -// app. -func (m *Manifest) ValidateApp() (bool, error) { - switch m.Library { - case "numerous": - return m.validateNumerousApp() - default: - return true, nil - } -} - -func (m *Manifest) validateNumerousApp() (bool, error) { - data, err := os.ReadFile(m.AppFile) - if err != nil { - return false, err - } - - filecontent := string(data) - if strings.Contains(filecontent, "appdef =") || strings.Contains(filecontent, "class appdef") { - return true, nil - } else { - output.PrintError("Your app file must have an app definition called 'appdef'", strings.Join( - []string{ - "You can solve this by assigning your app definition to this name, for example:", - "", - "@app", - "class MyApp:", - " my_field: str", - "", - "appdef = MyApp", - }, "\n"), - ) - - return false, nil - } -} diff --git a/cli/manifest/manifest_test.go b/cli/manifest/manifest_test.go index 0c75d48..6680d34 100644 --- a/cli/manifest/manifest_test.go +++ b/cli/manifest/manifest_test.go @@ -42,7 +42,7 @@ type encodeTOMLTestCase struct { var streamlitManifest = Manifest{ Name: "Tool Name", Description: "A description", - Library: "streamlit", + Library: LibraryStreamlit, Python: "3.11", Port: 80, AppFile: "app.py", @@ -193,8 +193,12 @@ func TestManifestValidateApp(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { appfile := test.WriteTempFile(t, "appfile.py", []byte(testCase.appfileContent)) - m := Manifest{Library: testCase.library, AppFile: appfile} + l, err := GetLibraryByKey(testCase.library) + require.NoError(t, err) + + m := Manifest{Library: l, AppFile: appfile} validated, err := m.ValidateApp() + assert.NoError(t, err) assert.Equal(t, testCase.expected, validated) }) diff --git a/cli/manifest/validate_app.go b/cli/manifest/validate_app.go new file mode 100644 index 0000000..89d5db5 --- /dev/null +++ b/cli/manifest/validate_app.go @@ -0,0 +1,50 @@ +package manifest + +import ( + "os" + "strings" + + "numerous/cli/cmd/output" +) + +// Validates that the given app file is valid for this library. Returns false, +// if the app is in a state, where it does not make sense, to be able to push +// the app. +func (m *Manifest) ValidateApp() (bool, error) { + return m.Library.ValidateApp(m.AppFile) +} + +func (l Library) ValidateApp(appFile string) (bool, error) { + switch l.Key { + case "numerous": + return validateNumerousApp(appFile) + default: + return true, nil + } +} + +func validateNumerousApp(appFile string) (bool, error) { + data, err := os.ReadFile(appFile) + if err != nil { + return false, err + } + + filecontent := string(data) + if strings.Contains(filecontent, "appdef =") || strings.Contains(filecontent, "class appdef") { + return true, nil + } else { + output.PrintError("Your app file must have an app definition called 'appdef'", strings.Join( + []string{ + "You can solve this by assigning your app definition to this name, for example:", + "", + "@app", + "class MyApp:", + " my_field: str", + "", + "appdef = MyApp", + }, "\n"), + ) + + return false, nil + } +}