Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add project scaffolding option #5

Merged
merged 3 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type FormResponse struct {
UseDbtProfile bool
DbtProfile string
DbtProfileOutput string
CreateProfile bool
ScaffoldProject bool
ProjectName string
}

func Forms() (formResponse FormResponse) {
Expand All @@ -41,7 +44,7 @@ Generates:
For each table in the designated schema/dataset.

To prepare, make sure you have the following:
✴︎ An existing dbt profile.yml file to reference
✴︎ An existing dbt profiles.yml file to reference
*_OR_*
✴︎ The necessary connection details for your warehouse

Expand Down Expand Up @@ -75,8 +78,23 @@ You'll need:
huh.NewConfirm().Affirmative("Yes!").Negative("Nah").
Title("Do you have a dbt profile you'd like to connect with?\n(you can enter your credentials manually if not)").
Value(&formResponse.UseDbtProfile),
huh.NewConfirm().Affirmative("Yeah!").Negative("Nope").
Title("Would you like to scaffold a basic dbt project into the output directory?").
Value(&formResponse.ScaffoldProject),
),
)
project_name_form := huh.NewForm(
huh.NewGroup(huh.NewInput().
Title("What is the name of your dbt project?").
Value(&formResponse.ProjectName).
Placeholder("gondor_patrol_analytics"),
))
profile_create_form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().Affirmative("Yes, pls").Negative("No, thx").
Title("Would you like to generate a profiles.yml file from the info you provide next?").
Value(&formResponse.CreateProfile),
))
dbt_form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Expand Down Expand Up @@ -155,8 +173,8 @@ Relative to pwd e.g. if db is in this dir -> cool_ducks.db`).
huh.NewNote().
Title("🚧🚨 Choose your build directory carefully! 🚨🚧").
Description(`Choose a _new_ or _empty_ directory.
If you use an existing directory,
tbd will overwrite any existing files of the same name.`),
If you choose an existing, populated directory
tbd will _intentionally error out_.`),
),
huh.NewGroup(
huh.NewInput().
Expand All @@ -173,6 +191,8 @@ tbd will overwrite any existing files of the same name.`),
),
)
intro_form.WithTheme(huh.ThemeCatppuccin())
profile_create_form.WithTheme(huh.ThemeCatppuccin())
project_name_form.WithTheme(huh.ThemeCatppuccin())
dbt_form.WithTheme(huh.ThemeCatppuccin())
warehouse_form.WithTheme(huh.ThemeCatppuccin())
snowflake_form.WithTheme(huh.ThemeCatppuccin())
Expand All @@ -191,6 +211,16 @@ tbd will overwrite any existing files of the same name.`),
log.Fatalf("Error running dbt form %v\n", err)
}
} else {
err = profile_create_form.Run()
if err != nil {
log.Fatalf("Error running profile create form %v\n", err)
}
if formResponse.ScaffoldProject {
err = project_name_form.Run()
if err != nil {
log.Fatalf("Error running project name form %v\n", err)
}
}
err = warehouse_form.Run()
if err != nil {
log.Fatalf("Error running warehouse form %v\n", err)
Expand Down
15 changes: 14 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,20 @@ func main() {
if formResponse.GenerateDescriptions {
GenerateColumnDescriptions(ts)
}
PrepBuildDir(bd)
err = PrepBuildDir(bd)
if err != nil {
log.Fatalf("Error preparing build directory: %v\n", err)
}
if formResponse.CreateProfile {
WriteProfile(cd, bd)
}
if formResponse.ScaffoldProject {
s, err := WriteScaffoldProject(cd, bd, formResponse.ProjectName)
if err != nil {
log.Fatalf("Error scaffolding project: %v\n", err)
}
bd = s
}
err = WriteFiles(ts, bd)
if err != nil {
log.Fatalf("Error writing files: %v\n", err)
Expand Down
22 changes: 18 additions & 4 deletions prep_build_dir.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
package main

import (
"errors"
"log"
"os"
)

func PrepBuildDir(buildDir string) {
_, err := os.Stat(buildDir)
func PrepBuildDir(bd string) error {
_, err := os.Stat(bd)
if os.IsNotExist(err) {
dirErr := os.MkdirAll(buildDir, 0755)
dirErr := os.MkdirAll(bd, 0755)
if dirErr != nil {
log.Fatalf("Failed to create directory %v", dirErr)
return dirErr
}
} else if err == nil {
files, err := os.ReadDir(bd)
if err != nil {
log.Fatalf("Failed to check build target directory %v", err)
}
if len(files) == 0 {
return nil
} else {
return errors.New("build directory is not empty")
}
} else {
return err
}
return nil
}
17 changes: 9 additions & 8 deletions shared/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ type SourceTables struct {
}

type ConnectionDetails struct {
ConnType string
Username string
Account string
Database string
Schema string
Project string
Dataset string
Path string
ConnType string
Username string
Account string
Database string
Schema string
Project string
Dataset string
Path string
ProjectName string
}
62 changes: 62 additions & 0 deletions write_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
"log"
"os"
"path"
"text/template"

"github.com/gwenwindflower/tbd/shared"
)

func WriteProfile(cd shared.ConnectionDetails, bd string) {
pt := `
{{.ConnType}}:
target: dev
outputs:
dev:
type: {{.ConnType}}
{{- if eq .ConnType "snowflake"}}
authenticator: externalbrowser
{{- end}}
{{- if eq .ConnType "bigquery"}}
method: oauth
{{- end}}
{{- if .Account}}
account: {{.Account}}
{{- end}}
{{- if .Username}}
user: {{.Username}}
{{- end}}
{{- if .Database}}
database: {{.Database}}
{{- end}}
{{- if .Project}}
project: {{.Project}}
{{- end}}
{{- if .Schema}}
schema: {{.Schema}}
{{- end}}
{{- if .Dataset}}
dataset: {{.Dataset}}
{{- end}}
{{- if .Path}}
path: {{.Path}}
{{- end}}
threads: 8
`
tmpl, err := template.New("profiles").Parse(pt)
if err != nil {
log.Fatalf("Failed to parse template %v\n", err)
}
p := path.Join(bd, "profiles.yml")
o, err := os.Create(p)
if err != nil {
log.Fatalf("Failed to create profiles.yml file %v\n", err)
}
defer o.Close()
err = tmpl.Execute(o, cd)
if err != nil {
log.Fatalf("Failed to execute template %v\n", err)
}
}
44 changes: 44 additions & 0 deletions write_profile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"os"
"path"
"testing"

"github.com/gwenwindflower/tbd/shared"
)

func TestWriteProfile(t *testing.T) {
cd := shared.ConnectionDetails{
ConnType: "snowflake",
Username: "aragorn",
Account: "dunedain.snowflakecomputing.com",
Database: "gondor",
Schema: "minas_tirith",
}
tmpDir := t.TempDir()
WriteProfile(cd, tmpDir)

expected := []byte(`
snowflake:
target: dev
outputs:
dev:
type: snowflake
authenticator: externalbrowser
account: dunedain.snowflakecomputing.com
user: aragorn
database: gondor
schema: minas_tirith
threads: 8
`)
tpp := path.Join(tmpDir, "profiles.yml")
got, err := os.ReadFile(tpp)
if err != nil {
t.Fatalf("Failed to read profiles.yml: %v", err)
}
// os.Remove("profiles.yml")
if string(got) != string(expected) {
t.Errorf("Expected %s, got %s", expected, got)
}
}
97 changes: 97 additions & 0 deletions write_scaffold_project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package main

import (
"log"
"os"
"path"
"text/template"

"github.com/gwenwindflower/tbd/shared"
)

func WriteScaffoldProject(cd shared.ConnectionDetails, bd string, pn string) (string, error) {
folders := []string{"models", "analyses", "macros", "seeds", "snapshots", "data-tests", "models/staging", "models/marts"}
emptyFolders := []string{"analyses", "macros", "seeds", "snapshots", "data-tests", "models/marts"}
for _, folder := range folders {
p := path.Join(bd, folder)
err := os.MkdirAll(p, 0755)
if err != nil {
return "", err
}
}
for _, folder := range emptyFolders {
p := path.Join(bd, folder, ".gitkeep")
err := os.MkdirAll(p, 0755)
if err != nil {
log.Fatalf("Failed to create .gitkeep in %s folder %v\n", folder, err)
}
}
projectYamlTemplate := `config-version: 2

name: {{.ProjectName}}
profile: {{.ConnType}}

model-paths: ["models"]
analysis-paths: ["analyses"]
test-paths: ["data-tests"]
seed-paths: ["seeds"]
macro-paths: ["macros"]
snapshot-paths: ["snapshots"]

target-path: "target"
clean-targets:
- "target"
- "dbt_packages"

models:
{{.ProjectName}}:
staging:
+materialized: view
marts:
+materialized: table
`
gitignore := []byte(`.venv
venv
.env
env

target/
dbt_packages/
logs/
profiles.yml

.DS_Store

.user.yml

.ruff_cache
__pycache__
`)

tmpl, err := template.New("dbt_project.yml").Parse(projectYamlTemplate)
if err != nil {
log.Fatalf("Failed to parse dbt_project.yml template %v\n", err)
}
p := path.Join(bd, "dbt_project.yml")
o, err := os.Create(p)
if err != nil {
log.Fatalf("Failed to create dbt_project.yml file %v\n", err)
}
defer o.Close()
cd.ProjectName = pn
err = tmpl.Execute(o, cd)
if err != nil {
log.Fatalf("Failed to execute dbt_project.yml template %v\n", err)
}
gi := path.Join(bd, ".gitignore")
err = os.WriteFile(gi, gitignore, 0644)
if err != nil {
log.Fatalf("Failed to write .gitignore file %v\n", err)
}
s := path.Join(bd, "models/staging", cd.Schema)
err = os.MkdirAll(s, 0755)
if err != nil {
return "", err
}
return s, nil
}
Loading