diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fad514..5d7cdba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ on: - main env: - VERSION_NUMBER: 'v1.6.2' + VERSION_NUMBER: 'v1.7.0' DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli' AWS_REGION: 'us-west-2' diff --git a/.gitignore b/.gitignore index 969d117..4e571b1 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ card_data/.tmp*/** card_data/pipelines/poke_cli_dbt/.user.yml /card_data/supabase/ + +card_data/~/ diff --git a/.goreleaser.yml b/.goreleaser.yml index c5acb37..d1c26aa 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,7 +14,7 @@ builds: - windows - darwin ldflags: - - -s -w -X main.version=v1.6.2 + - -s -w -X main.version=v1.7.0 archives: - formats: [ 'zip' ] diff --git a/Dockerfile b/Dockerfile index 65e96cf..a8f9dd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN go mod download COPY . . -RUN go build -ldflags "-X main.version=v1.6.2" -o poke-cli . +RUN go build -ldflags "-X main.version=v1.7.0" -o poke-cli . # build 2 FROM --platform=$BUILDPLATFORM alpine:3.22 diff --git a/README.md b/README.md index 462965a..b3cd32e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ pokemon-logo

Pokémon CLI

version-label - docker-image-size + docker-image-size ci-status-badge
@@ -91,11 +91,11 @@ Cloudsmith is a fully cloud-based service that lets you easily create, store, an 3. Choose how to interact with the container: * Run a single command and exit: ```bash - docker run --rm -it digitalghostdev/poke-cli:v1.6.2 [subcommand] flag] + docker run --rm -it digitalghostdev/poke-cli:v1.7.0 [subcommand] flag] ``` * Enter the container and use its shell: ```bash - docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.6.2 -c "cd /app && exec sh" + docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.7.0 -c "cd /app && exec sh" # placed into the /app directory, run the program with './poke-cli' # example: ./poke-cli ability swift-swim ``` @@ -158,6 +158,7 @@ By running `poke-cli [-h | --help]`, it'll display information on how to use the │ │ │ COMMANDS: │ │ ability Get details about an ability │ +│ berry Get details about a berry │ │ item Get details about an item │ │ move Get details about a move │ │ natures Get details about all natures │ @@ -182,7 +183,7 @@ Below is a list of the planned/completed commands and flags: - [x] `ability`: get data about an ability. - [x] `-p | --pokemon`: display Pokémon that learn this ability. -- [ ] `berry`: get data about a berry. +- [x] `berry`: get data about a berry. - [x] `item`: get data about an item. - [x] `move`: get data about a move. - [ ] `-p | --pokemon`: display Pokémon that learn this move. diff --git a/card_data/pipelines/defs/transformation/transform_data.py b/card_data/pipelines/defs/transformation/transform_data.py index add3303..d01b185 100644 --- a/card_data/pipelines/defs/transformation/transform_data.py +++ b/card_data/pipelines/defs/transformation/transform_data.py @@ -2,42 +2,17 @@ from dagster_dbt import DbtCliResource, dbt_assets from pathlib import Path -from ..load.load_data import ( - load_series_data, - data_quality_check_on_series, - load_set_data, - load_card_data -) - DBT_PROJECT_PATH = Path(__file__).joinpath("..", "..", "..", "poke_cli_dbt").resolve() -@dg.asset(deps=[load_series_data, data_quality_check_on_series, load_set_data, load_card_data], kinds=["dbt"]) -def dbt_transformation(context: dg.AssetExecutionContext): - """Run dbt build after all extract and load operations complete""" - import subprocess - import os - - # Set environment variables for dbt - env = os.environ.copy() - env["SUPABASE_PASSWORD"] = os.getenv("SUPABASE_PASSWORD", "") - - # Run dbt build - result = subprocess.run( - ["dbt", "build"], - cwd=str(DBT_PROJECT_PATH), - env=env, - capture_output=True, - text=True - ) - - if result.returncode != 0: - context.log.error(f"dbt build failed: {result.stderr}") - raise Exception(f"dbt build failed: {result.stderr}") - - context.log.info(f"dbt build completed successfully: {result.stdout}") - return "dbt build completed" +@dbt_assets(manifest=DBT_PROJECT_PATH / "target" / "manifest.json") +def poke_cli_dbt_assets(context: dg.AssetExecutionContext, dbt: DbtCliResource): + """ + dbt assets that transform staging data into final models. + """ + yield from dbt.cli(["build"], context=context).stream() -# Create definitions for this transformation +dbt_resource = DbtCliResource(project_dir=DBT_PROJECT_PATH) defs = dg.Definitions( - assets=[dbt_transformation] + assets=[poke_cli_dbt_assets], + resources={"dbt": dbt_resource} ) diff --git a/card_data/pipelines/poke_cli_dbt/dbt_project.yml b/card_data/pipelines/poke_cli_dbt/dbt_project.yml new file mode 100644 index 0000000..6c02b90 --- /dev/null +++ b/card_data/pipelines/poke_cli_dbt/dbt_project.yml @@ -0,0 +1,24 @@ +name: 'poke_cli_dbt' +version: '1.7.0' + +profile: 'poke_cli_dbt' + +model-paths: ["models"] +analysis-paths: ["analyses"] +test-paths: ["tests"] +seed-paths: ["seeds"] +macro-paths: ["macros"] +snapshot-paths: ["snapshots"] + +# directories removed by 'dbt clean' +clean-targets: + - "target" + - "dbt_packages" + +models: + poke_cli_dbt: + # Transform staging data to public schema + +materialized: table + +on-run-end: + - "{{ create_relationships() }}" \ No newline at end of file diff --git a/card_data/pipelines/poke_cli_dbt/macros/create_relationships.sql b/card_data/pipelines/poke_cli_dbt/macros/create_relationships.sql new file mode 100644 index 0000000..70f52fe --- /dev/null +++ b/card_data/pipelines/poke_cli_dbt/macros/create_relationships.sql @@ -0,0 +1,5 @@ +{% macro create_relationships() %} + ALTER TABLE {{ ref('series') }} ADD CONSTRAINT pk_series PRIMARY KEY (id); + ALTER TABLE {{ ref('sets') }} ADD CONSTRAINT pk_sets PRIMARY KEY (set_id); + ALTER TABLE {{ ref('cards') }} ADD CONSTRAINT pk_cards PRIMARY KEY (id); +{% endmacro %} \ No newline at end of file diff --git a/card_data/pipelines/poke_cli_dbt/macros/create_rls.sql b/card_data/pipelines/poke_cli_dbt/macros/create_rls.sql new file mode 100644 index 0000000..89b55a1 --- /dev/null +++ b/card_data/pipelines/poke_cli_dbt/macros/create_rls.sql @@ -0,0 +1,4 @@ +{% macro enable_rls() %} + ALTER TABLE {{ this }} ENABLE ROW LEVEL SECURITY; + CREATE POLICY "Enable read access for all users" ON {{ this }} TO PUBLIC USING (true); +{% endmacro %} \ No newline at end of file diff --git a/card_data/pipelines/poke_cli_dbt/models/cards.sql b/card_data/pipelines/poke_cli_dbt/models/cards.sql index 1db98ed..dfcaad7 100644 --- a/card_data/pipelines/poke_cli_dbt/models/cards.sql +++ b/card_data/pipelines/poke_cli_dbt/models/cards.sql @@ -1,4 +1,7 @@ -{{ config(materialized='table') }} +{{ config( + materialized='table', + post_hook="{{ enable_rls() }}" +) }} -SELECT id, image, name, "localId", category -FROM {{ source('staging', 'cards') }} +SELECT id, image, name, "localId", category, hp +FROM {{ source('staging', 'cards') }} \ No newline at end of file diff --git a/card_data/pipelines/poke_cli_dbt/models/series.sql b/card_data/pipelines/poke_cli_dbt/models/series.sql new file mode 100644 index 0000000..ff0e3cc --- /dev/null +++ b/card_data/pipelines/poke_cli_dbt/models/series.sql @@ -0,0 +1,7 @@ +{{ config( + materialized='table', + post_hook="{{ enable_rls() }}" +) }} + +SELECT * +FROM {{ source('staging', 'series') }} \ No newline at end of file diff --git a/card_data/pipelines/poke_cli_dbt/models/sets.sql b/card_data/pipelines/poke_cli_dbt/models/sets.sql new file mode 100644 index 0000000..926082c --- /dev/null +++ b/card_data/pipelines/poke_cli_dbt/models/sets.sql @@ -0,0 +1,7 @@ +{{ config( + materialized='table', + post_hook="{{ enable_rls() }}" +) }} + +SELECT * +FROM {{ source('staging', 'sets') }} \ No newline at end of file diff --git a/card_data/pipelines/soda/configuration.yml b/card_data/pipelines/soda/configuration.yml index f4b67d8..dc4283d 100644 --- a/card_data/pipelines/soda/configuration.yml +++ b/card_data/pipelines/soda/configuration.yml @@ -3,7 +3,7 @@ data_source supabase: connection: host: aws-0-us-east-2.pooler.supabase.com port: '5432' - username: ${POSTGRES_USERNAME} - password: ${POSTGRES_PASSWORD} + username: ${SUPABASE_USER} + password: ${SUPABASE_PASSWORD} database: postgres - schema: poke_data + schema: staging diff --git a/card_data/pipelines/utils/secret_retriever.py b/card_data/pipelines/utils/secret_retriever.py new file mode 100644 index 0000000..62ca787 --- /dev/null +++ b/card_data/pipelines/utils/secret_retriever.py @@ -0,0 +1,18 @@ +import botocore +import botocore.session +from aws_secretsmanager_caching import SecretCache, SecretCacheConfig + +import json + + +def fetch_secret() -> str: + client = botocore.session.get_session().create_client("secretsmanager") + cache_config = SecretCacheConfig() + cache = SecretCache(config=cache_config, client=client) + + secret = cache.get_secret_string("supabase-data") + + # convert to dictionary + secret_dict = json.loads(secret) + + return secret_dict["database_uri"] diff --git a/card_data/pyproject.toml b/card_data/pyproject.toml index 615765a..e14f49f 100644 --- a/card_data/pyproject.toml +++ b/card_data/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "aws-secretsmanager-caching>=1.1.3", + "beautifulsoup4>=4.13.5", "dagster>=1.11.3", "dagster-dbt>=0.27.3", "dagster-dg-cli>=1.11.3", diff --git a/card_data/uv.lock b/card_data/uv.lock index 3744546..c4547e0 100644 --- a/card_data/uv.lock +++ b/card_data/uv.lock @@ -105,6 +105,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, +] + [[package]] name = "botocore" version = "1.39.4" @@ -125,6 +138,7 @@ version = "1.5.2" source = { virtual = "." } dependencies = [ { name = "aws-secretsmanager-caching" }, + { name = "beautifulsoup4" }, { name = "dagster" }, { name = "dagster-dbt" }, { name = "dagster-dg-cli" }, @@ -145,6 +159,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "dagster-dbt" }, { name = "dagster-dg-cli" }, { name = "dagster-postgres" }, { name = "dagster-webserver" }, @@ -153,6 +168,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aws-secretsmanager-caching", specifier = ">=1.1.3" }, + { name = "beautifulsoup4", specifier = ">=4.13.5" }, { name = "dagster", specifier = ">=1.11.3" }, { name = "dagster-dbt", specifier = ">=0.27.3" }, { name = "dagster-dg-cli", specifier = ">=1.11.3" }, @@ -173,6 +189,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "dagster-dbt", specifier = ">=0.27.3" }, { name = "dagster-dg-cli" }, { name = "dagster-postgres", specifier = ">=0.27.3" }, { name = "dagster-webserver" }, @@ -2103,6 +2120,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/7e/5b7a4110673af32edd8841abfe1e4db48e1ccb8675eae78bc2962d894f9e/soda_core_postgres-3.5.5-py3-none-any.whl", hash = "sha256:6201aecb22b9150cc9673f7113d1ad4a236d15e04460ede5db7430437527cf6c", size = 10230, upload-time = "2025-06-12T12:28:17.464Z" }, ] +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.41" diff --git a/cli.go b/cli.go index 98808b1..2752f61 100644 --- a/cli.go +++ b/cli.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/digitalghost-dev/poke-cli/cmd/ability" + "github.com/digitalghost-dev/poke-cli/cmd/berry" "github.com/digitalghost-dev/poke-cli/cmd/item" "github.com/digitalghost-dev/poke-cli/cmd/move" "github.com/digitalghost-dev/poke-cli/cmd/natures" @@ -69,6 +70,7 @@ func runCLI(args []string) int { fmt.Sprintf("\n\t%-15s %s", "-v, --version", "Prints the current version"), "\n\n", styling.StyleBold.Render("COMMANDS:"), fmt.Sprintf("\n\t%-15s %s", "ability", "Get details about an ability"), + fmt.Sprintf("\n\t%-15s %s", "berry", "Get details about a berry"), fmt.Sprintf("\n\t%-15s %s", "item", "Get details about an item"), fmt.Sprintf("\n\t%-15s %s", "move", "Get details about a move"), fmt.Sprintf("\n\t%-15s %s", "natures", "Get details about all natures"), @@ -104,6 +106,7 @@ func runCLI(args []string) int { commands := map[string]func() int{ "ability": utils.HandleCommandOutput(ability.AbilityCommand), + "berry": utils.HandleCommandOutput(berry.BerryCommand), "item": utils.HandleCommandOutput(item.ItemCommand), "move": utils.HandleCommandOutput(move.MoveCommand), "natures": utils.HandleCommandOutput(natures.NaturesCommand), @@ -140,6 +143,7 @@ func runCLI(args []string) int { fmt.Sprintf("\n\t%-15s", fmt.Sprintf("'%s' is not a valid command.\n", cmdArg)), styling.StyleBold.Render("\nCommands:"), fmt.Sprintf("\n\t%-15s %s", "ability", "Get details about an ability"), + fmt.Sprintf("\n\t%-15s %s", "berry", "Get details about a berry"), fmt.Sprintf("\n\t%-15s %s", "item", "Get details about an item"), fmt.Sprintf("\n\t%-15s %s", "move", "Get details about a move"), fmt.Sprintf("\n\t%-15s %s", "natures", "Get details about all natures"), diff --git a/cmd/berry/berry.go b/cmd/berry/berry.go new file mode 100644 index 0000000..e452ff6 --- /dev/null +++ b/cmd/berry/berry.go @@ -0,0 +1,158 @@ +package berry + +import ( + "flag" + "fmt" + "log" + "os" + "strings" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/digitalghost-dev/poke-cli/cmd/utils" + "github.com/digitalghost-dev/poke-cli/connections" + "github.com/digitalghost-dev/poke-cli/styling" +) + +func BerryCommand() (string, error) { + var output strings.Builder + + flag.Usage = func() { + helpMessage := styling.HelpBorder.Render( + "Get details about a specific berry.\n\n", + styling.StyleBold.Render("USAGE:"), + fmt.Sprintf("\n\t%s %s %s", "poke-cli", styling.StyleBold.Render("berry"), "[flag]"), + "\n\n", + styling.StyleBold.Render("FLAGS:"), + fmt.Sprintf("\n\t%-30s %s", "-h, --help", "Prints out the help menu"), + ) + output.WriteString(helpMessage) + } + + flag.Parse() + + // Handle help flag + if len(os.Args) == 3 && (os.Args[2] == "-h" || os.Args[2] == "--help") { + flag.Usage() + return output.String(), nil + } + + // Validate arguments + if err := utils.ValidateTypesArgs(os.Args); err != nil { + output.WriteString(err.Error()) + return output.String(), err + } + + tableGeneration() + + return output.String(), nil +} + +type model struct { + quitting bool + table table.Model + selectedOption string +} + +// Init initializes the model +func (m model) Init() tea.Cmd { + return nil +} + +// Update handles user input and updates the model state +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var bubbleCmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc", "ctrl+c": + m.quitting = true + return m, tea.Quit + } + } + + m.table, bubbleCmd = m.table.Update(msg) + + // Keep the selected option in sync on every update + if row := m.table.SelectedRow(); len(row) > 0 { + name := row[0] + if name != m.selectedOption { + m.selectedOption = name + } + } + + return m, bubbleCmd +} + +// View renders the current UI +func (m model) View() string { + if m.quitting { + return "\n Goodbye! \n" + } + + selectedBerry := "" + if row := m.table.SelectedRow(); len(row) > 0 { + selectedBerry = BerryName(row[0]) + "\n---\n" + BerryEffect(row[0]) + "\n---\n" + BerryInfo(row[0]) + "\n---\nImage\n" + BerryImage(row[0]) + } + + leftPanel := styling.TypesTableBorder.Render(m.table.View()) + + rightPanel := lipgloss.NewStyle(). + Width(50). + Height(29). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#FFCC00")). + Padding(1). + Render(selectedBerry) + + screen := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, rightPanel) + + return fmt.Sprintf("Highlight a berry!\n%s\n%s", + screen, + styling.KeyMenu.Render("↑ (move up) • ↓ (move down)\nctrl+c | esc (quit)")) +} + +func tableGeneration() { + namesList, err := connections.QueryBerryData(` + SELECT + UPPER(SUBSTR(name, 1, 1)) || SUBSTR(name, 2) + FROM + berries + ORDER BY + name`) + if err != nil { + log.Fatalf("Failed to get berry names: %v", err) + } + + rows := make([]table.Row, len(namesList)) + for i, n := range namesList { + rows[i] = []string{n} + } + + t := table.New( + table.WithColumns([]table.Column{{Title: "Berry", Width: 16}}), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(28), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#FFCC00")). + BorderBottom(true) + s.Selected = s.Selected. + Foreground(lipgloss.Color("#000")). + Background(lipgloss.Color("#FFCC00")) + t.SetStyles(s) + + m := model{table: t} + _, err = tea.NewProgram(m).Run() + + if err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/cmd/berry/berry_test.go b/cmd/berry/berry_test.go new file mode 100644 index 0000000..fee3ec9 --- /dev/null +++ b/cmd/berry/berry_test.go @@ -0,0 +1,161 @@ +package berry + +import ( + "os" + "strings" + "testing" + + "github.com/charmbracelet/bubbles/table" +) + +func TestBerryCommand(t *testing.T) { + tests := []struct { + name string + args []string + wantErr bool + contains string + }{ + { + name: "help flag short", + args: []string{"poke-cli", "berry", "-h"}, + wantErr: false, + contains: "USAGE:", + }, + { + name: "help flag long", + args: []string{"poke-cli", "berry", "--help"}, + wantErr: false, + contains: "FLAGS:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up os.Args for the test + oldArgs := os.Args + os.Args = tt.args + defer func() { os.Args = oldArgs }() + + output, err := BerryCommand() + + if (err != nil) != tt.wantErr { + t.Errorf("BerryCommand() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.contains != "" && !strings.Contains(output, tt.contains) { + t.Errorf("BerryCommand() output should contain %q, got %q", tt.contains, output) + } + }) + } +} + +func TestModelInit(t *testing.T) { + m := model{} + cmd := m.Init() + if cmd != nil { + t.Errorf("Init() should return nil, got %v", cmd) + } +} + +func TestModelUpdate(t *testing.T) { + // Create a simple table for testing + columns := []table.Column{{Title: "Berry", Width: 16}} + rows := []table.Row{{"TestBerry"}} + testTable := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(5), + ) + + m := model{ + table: testTable, + } + + tests := []struct { + name string + keyMsg string + shouldQuit bool + expectError bool + }{ + { + name: "escape key", + keyMsg: "esc", + shouldQuit: true, + }, + { + name: "ctrl+c key", + keyMsg: "ctrl+c", + shouldQuit: true, + }, + { + name: "other key", + keyMsg: "j", + shouldQuit: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + updated, _ := m.Update(nil) + + if tt.shouldQuit { + if updated == nil { + t.Errorf("Update() returned nil model") + } + } + }) + } +} + +func TestModelView(t *testing.T) { + // Test with empty table + m := model{ + quitting: false, + table: table.New(), + } + + view := m.View() + if view == "" { + t.Errorf("View() should not return empty string for normal state") + } + + // Test quitting state + m.quitting = true + view = m.View() + if !strings.Contains(view, "Goodbye") { + t.Errorf("View() should contain 'Goodbye' when quitting, got %q", view) + } +} + +func TestModelViewWithSelectedBerry(t *testing.T) { + // Create a table with test data + columns := []table.Column{{Title: "Berry", Width: 16}} + rows := []table.Row{{"Aguav"}} + testTable := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(5), + ) + + m := model{ + table: testTable, + } + + view := m.View() + + // Should contain the main UI elements + expectedElements := []string{ + "Highlight a berry!", + "↑ (move up) • ↓ (move down)", + "ctrl+c | esc (quit)", + } + + for _, element := range expectedElements { + if !strings.Contains(view, element) { + t.Errorf("View() should contain %q, got %q", element, view) + } + } +} diff --git a/cmd/berry/berryinfo.go b/cmd/berry/berryinfo.go new file mode 100644 index 0000000..534dc4e --- /dev/null +++ b/cmd/berry/berryinfo.go @@ -0,0 +1,127 @@ +package berry + +import ( + "image" + "net/http" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/digitalghost-dev/poke-cli/connections" + "github.com/digitalghost-dev/poke-cli/styling" + "github.com/disintegration/imaging" +) + +// BerryName prints information based on currently selected berry. +func BerryName(berryName string) string { + return "Berry: " + berryName +} + +func BerryEffect(berryName string) string { + berryEffect, err := connections.QueryBerryData(` + SELECT + effect + FROM + berries + WHERE + UPPER(SUBSTR(name, 1, 1)) || SUBSTR(name, 2) = ?`, + berryName, + ) + + if err != nil || len(berryEffect) == 0 || berryEffect[0] == "" { + return "Effect information not available" + } + + return berryEffect[0] +} + +func BerryInfo(berryName string) string { + berryInfo, err := connections.QueryBerryData(` + SELECT + 'Firmness: ' || firmness || char(10) || + 'Smoothness: ' || smoothness || char(10) || + 'Growth Time: ' || growth_time || ' hours' || char(10) || + 'Max Harvest: ' || max_harvest + FROM + berries + WHERE + UPPER(SUBSTR(name, 1, 1)) || SUBSTR(name, 2) = ?`, + berryName, + ) + + if err != nil || len(berryInfo) == 0 || berryInfo[0] == "" { + return "Additional information not available" + } + + return berryInfo[0] +} + +func BerryImage(berryName string) string { + berryImage, err := connections.QueryBerryData(` + SELECT + sprite_url + FROM + berries + WHERE + UPPER(SUBSTR(name, 1, 1)) || SUBSTR(name, 2) = ?`, + berryName, + ) + + if err != nil || len(berryImage) == 0 || berryImage[0] == "" { + return "Image information not available" + } + + ToString := func(width int, height int, img image.Image) string { + img = imaging.Resize(img, width, height, imaging.NearestNeighbor) + b := img.Bounds() + + imageWidth := b.Max.X + h := b.Max.Y + + rowCount := (h - 1) / 2 + if h%2 != 0 { + rowCount++ + } + estimatedSize := (imageWidth * rowCount * 10) + rowCount + + str := strings.Builder{} + str.Grow(estimatedSize) + + styleCache := make(map[string]lipgloss.Style) + + for heightCounter := 0; heightCounter < h-1; heightCounter += 2 { + for x := 0; x < imageWidth; x++ { + // Get the color of the current and next row's pixels + c1, _ := styling.MakeColor(img.At(x, heightCounter)) + color1 := lipgloss.Color(c1.Hex()) + c2, _ := styling.MakeColor(img.At(x, heightCounter+1)) + color2 := lipgloss.Color(c2.Hex()) + + styleKey := string(color1) + "_" + string(color2) + style, exists := styleCache[styleKey] + if !exists { + style = lipgloss.NewStyle().Foreground(color1).Background(color2) + styleCache[styleKey] = style + } + + str.WriteString(style.Render("▀")) + } + + str.WriteString("\n") + } + + return str.String() + } + + imageResp, err := http.Get(berryImage[0]) + if err != nil { + return "Error downloading berry image" + } + defer imageResp.Body.Close() + + img, err := imaging.Decode(imageResp.Body) + if err != nil { + return "Error decoding berry image" + } + + return ToString(28, 28, img) +} diff --git a/cmd/berry/berryinfo_test.go b/cmd/berry/berryinfo_test.go new file mode 100644 index 0000000..1e901ad --- /dev/null +++ b/cmd/berry/berryinfo_test.go @@ -0,0 +1,217 @@ +package berry + +import ( + "image" + "image/color" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestBerryName(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "simple berry name", + input: "Aguav", + expected: "Berry: Aguav", + }, + { + name: "empty string", + input: "", + expected: "Berry: ", + }, + { + name: "berry with special characters", + input: "Test-Berry", + expected: "Berry: Test-Berry", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := BerryName(tt.input) + if result != tt.expected { + t.Errorf("BerryName(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestBerryEffect(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "non-existent berry", + input: "NonExistentBerry", + expected: "Effect information not available", + }, + { + name: "empty string", + input: "", + expected: "Effect information not available", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := BerryEffect(tt.input) + if tt.input == "NonExistentBerry" || tt.input == "" { + if result != tt.expected { + t.Errorf("BerryEffect(%q) = %q, want %q", tt.input, result, tt.expected) + } + } + }) + } +} + +func TestBerryInfo(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "non-existent berry", + input: "NonExistentBerry", + expected: "Additional information not available", + }, + { + name: "empty string", + input: "", + expected: "Additional information not available", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := BerryInfo(tt.input) + if tt.input == "NonExistentBerry" || tt.input == "" { + if result != tt.expected { + t.Errorf("BerryInfo(%q) = %q, want %q", tt.input, result, tt.expected) + } + } + }) + } +} + +func TestBerryImageWithMockServer(t *testing.T) { + // Create a mock HTTP server that serves a simple test image + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Create a simple 2x2 test image + img := image.NewRGBA(image.Rect(0, 0, 2, 2)) + img.Set(0, 0, color.RGBA{255, 0, 0, 255}) // Red + img.Set(1, 0, color.RGBA{0, 255, 0, 255}) // Green + img.Set(0, 1, color.RGBA{0, 0, 255, 255}) // Blue + img.Set(1, 1, color.RGBA{255, 255, 0, 255}) // Yellow + + w.Header().Set("Content-Type", "image/png") + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + result := BerryImage("NonExistentBerry") + expected := "Image information not available" + if result != expected { + t.Errorf("BerryImage('NonExistentBerry') = %q, want %q", result, expected) + } + + result = BerryImage("") + if result != expected { + t.Errorf("BerryImage('') = %q, want %q", result, expected) + } +} + +func TestBerryImageErrorHandling(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "non-existent berry", + input: "NonExistentBerry", + expected: "Image information not available", + }, + { + name: "empty string", + input: "", + expected: "Image information not available", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := BerryImage(tt.input) + if result != tt.expected { + t.Errorf("BerryImage(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +// Test helper function to check if the ToString function structure is working +func TestToStringStructure(t *testing.T) { + // This test checks that the ToString function can handle basic cases + // without actually making HTTP requests or database queries + + // Create a simple test image + img := image.NewRGBA(image.Rect(0, 0, 4, 4)) + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + img.Set(x, y, color.RGBA{uint8(x * 63), uint8(y * 63), 100, 255}) + } + } + + // Test the ToString function indirectly by checking that BerryImage + // with invalid input returns the expected error message + result := BerryImage("InvalidBerry") + if !strings.Contains(result, "information not available") { + t.Errorf("Expected error message for invalid berry, got: %q", result) + } +} + +// Test for database query error handling +func TestBerryFunctionsErrorHandling(t *testing.T) { + testCases := []struct { + name string + function func(string) string + input string + contains string + }{ + { + name: "BerryEffect with invalid input", + function: BerryEffect, + input: "InvalidBerry123", + contains: "not available", + }, + { + name: "BerryInfo with invalid input", + function: BerryInfo, + input: "InvalidBerry123", + contains: "not available", + }, + { + name: "BerryImage with invalid input", + function: BerryImage, + input: "InvalidBerry123", + contains: "not available", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.function(tc.input) + if !strings.Contains(result, tc.contains) { + t.Errorf("Expected result to contain %q, got %q", tc.contains, result) + } + }) + } +} diff --git a/cmd/item/item.go b/cmd/item/item.go index ca2cb0a..56c0978 100644 --- a/cmd/item/item.go +++ b/cmd/item/item.go @@ -3,6 +3,9 @@ package item import ( "flag" "fmt" + "os" + "strings" + "github.com/charmbracelet/lipgloss" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/connections" @@ -10,8 +13,6 @@ import ( "github.com/digitalghost-dev/poke-cli/styling" "golang.org/x/text/cases" "golang.org/x/text/language" - "os" - "strings" ) func ItemCommand() (string, error) { diff --git a/cmd/item/item_test.go b/cmd/item/item_test.go index dccc619..57142ed 100644 --- a/cmd/item/item_test.go +++ b/cmd/item/item_test.go @@ -1,11 +1,12 @@ package item import ( + "os" + "testing" + "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" "github.com/stretchr/testify/assert" - "os" - "testing" ) func TestItemCommand(t *testing.T) { diff --git a/cmd/types/damage_table.go b/cmd/types/damage_table.go index 1f91e4f..f44e39f 100644 --- a/cmd/types/damage_table.go +++ b/cmd/types/damage_table.go @@ -2,14 +2,15 @@ package types import ( "fmt" + "os" + "strings" + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/term" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/styling" "golang.org/x/text/cases" "golang.org/x/text/language" - "os" - "strings" ) // DamageTable Function to display type details after a type is selected diff --git a/cmd/types/types.go b/cmd/types/types.go index 99cf401..7206bb2 100644 --- a/cmd/types/types.go +++ b/cmd/types/types.go @@ -3,13 +3,14 @@ package types import ( "flag" "fmt" + "os" + "strings" + "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" - "os" - "strings" ) func TypesCommand() (string, error) { diff --git a/cmd/types/types_test.go b/cmd/types/types_test.go index 64d6be5..5dd2997 100644 --- a/cmd/types/types_test.go +++ b/cmd/types/types_test.go @@ -1,6 +1,10 @@ package types import ( + "os" + "testing" + "time" + "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -8,9 +12,6 @@ import ( "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" "github.com/stretchr/testify/assert" - "os" - "testing" - "time" ) func TestTypesCommand(t *testing.T) { diff --git a/connections/berry_db.go b/connections/berry_db.go new file mode 100644 index 0000000..107dd39 --- /dev/null +++ b/connections/berry_db.go @@ -0,0 +1,70 @@ +package connections + +import ( + "database/sql" + _ "embed" + "fmt" + "os" + + _ "modernc.org/sqlite" +) + +//go:embed db/berries.db +var embeddedDB []byte + +func QueryBerryData(query string, args ...interface{}) ([]string, error) { + // Create temp file + tmpFile, err := os.CreateTemp("", "berries-*.db") + if err != nil { + return nil, fmt.Errorf("failed to create temp file: %w", err) + } + defer func() { + // Close file first + if closeErr := tmpFile.Close(); closeErr != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to close temp file: %v\n", closeErr) + } + + // Then remove it + if removeErr := os.Remove(tmpFile.Name()); removeErr != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to remove temp file %s: %v\n", tmpFile.Name(), removeErr) + } + }() + + // Write to temp file + if _, err := tmpFile.Write(embeddedDB); err != nil { + return nil, fmt.Errorf("failed to write embedded database: %w", err) + } + + // Open the temp database file + db, err := sql.Open("sqlite", tmpFile.Name()) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + fmt.Printf("failed to close database connection: %v\n", err) + } + }(db) + + rows, err := db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query berry data: %w", err) + } + defer rows.Close() + + var results []string + for rows.Next() { + var result string + if err := rows.Scan(&result); err != nil { + return nil, fmt.Errorf("failed to scan berry data: %w", err) + } + results = append(results, result) + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating rows: %w", err) + } + + return results, nil +} diff --git a/connections/berry_db_test.go b/connections/berry_db_test.go new file mode 100644 index 0000000..4395aad --- /dev/null +++ b/connections/berry_db_test.go @@ -0,0 +1,240 @@ +package connections + +import ( + "os" + "strings" + "testing" +) + +func TestQueryBerryData(t *testing.T) { + t.Run("basic query without parameters", func(t *testing.T) { + query := "SELECT name FROM berries LIMIT 1" + results, err := QueryBerryData(query) + + if err != nil { + t.Fatalf("QueryBerryData() error = %v", err) + } + + if len(results) == 0 { + t.Error("QueryBerryData() should return at least one result") + } + }) + + t.Run("query with parameters", func(t *testing.T) { + query := "SELECT name FROM berries WHERE name LIKE ? LIMIT 1" + results, err := QueryBerryData(query, "%a%") + + if err != nil { + t.Fatalf("QueryBerryData() error = %v", err) + } + + // Should return some results since many berry names contain 'a' + if len(results) == 0 { + t.Error("QueryBerryData() should return at least one result for berries containing 'a'") + } + }) + + t.Run("query with multiple parameters", func(t *testing.T) { + query := "SELECT name FROM berries WHERE name LIKE ? OR name LIKE ? LIMIT 5" + results, err := QueryBerryData(query, "%a%", "%e%") + + if err != nil { + t.Fatalf("QueryBerryData() error = %v", err) + } + + if len(results) == 0 { + t.Error("QueryBerryData() should return results for berries containing 'a' or 'e'") + } + }) + + t.Run("invalid query", func(t *testing.T) { + query := "SELECT invalid_column FROM non_existent_table" + _, err := QueryBerryData(query) + + if err == nil { + t.Error("QueryBerryData() should return an error for invalid query") + } + }) + + t.Run("empty query", func(t *testing.T) { + query := "" + _, err := QueryBerryData(query) + + t.Logf("Empty query result: error = %v", err) + }) + + t.Run("query with no results", func(t *testing.T) { + query := "SELECT name FROM berries WHERE name = ?" + results, err := QueryBerryData(query, "NonExistentBerryName12345") + + if err != nil { + t.Fatalf("QueryBerryData() error = %v", err) + } + + if len(results) != 0 { + t.Errorf("QueryBerryData() should return empty results for non-existent berry, got %v", results) + } + }) + + t.Run("count query", func(t *testing.T) { + query := "SELECT COUNT(*) FROM berries" + results, err := QueryBerryData(query) + + if err != nil { + t.Fatalf("QueryBerryData() error = %v", err) + } + + if len(results) != 1 { + t.Errorf("QueryBerryData() should return exactly one result for COUNT query, got %d", len(results)) + } + + if results[0] == "0" { + t.Error("QueryBerryData() COUNT should return more than 0 berries") + } + }) + + t.Run("specific berry data query", func(t *testing.T) { + // Try to get all berry names and verify structure + query := "SELECT name FROM berries ORDER BY name LIMIT 10" + results, err := QueryBerryData(query) + + if err != nil { + t.Fatalf("QueryBerryData() error = %v", err) + } + + if len(results) == 0 { + t.Error("QueryBerryData() should return berry names") + } + + for _, result := range results { + if result == "" { + t.Error("QueryBerryData() should not return empty berry names") + } + } + }) + + t.Run("validate berries table exists", func(t *testing.T) { + // Use a simpler query that works with the single-column return format + query := "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='berries'" + results, err := QueryBerryData(query) + + if err != nil { + t.Fatalf("QueryBerryData() error = %v", err) + } + + if len(results) == 0 || results[0] != "1" { + t.Error("QueryBerryData() berries table should exist") + } + }) + + t.Run("SQL injection protection", func(t *testing.T) { + maliciousInput := "'; DROP TABLE berries; --" + query := "SELECT name FROM berries WHERE name = ?" + + results, err := QueryBerryData(query, maliciousInput) + if err != nil { + t.Fatalf("Query with malicious input should not error: %v", err) + } + + if len(results) != 0 { + t.Errorf("expected no results for malicious input, got %v", results) + } + }) +} + +func TestQueryBerryDataWithVariadicArgs(t *testing.T) { + tests := []struct { + name string + query string + args []interface{} + expectError bool + }{ + { + name: "no args", + query: "SELECT COUNT(*) FROM berries", + args: []interface{}{}, + expectError: false, + }, + { + name: "one arg", + query: "SELECT name FROM berries WHERE name LIKE ? LIMIT 1", + args: []interface{}{"%a%"}, + expectError: false, + }, + { + name: "multiple args", + query: "SELECT name FROM berries WHERE name LIKE ? OR name LIKE ? LIMIT 1", + args: []interface{}{"%a%", "%e%"}, + expectError: false, + }, + { + name: "mixed types", + query: "SELECT name FROM berries LIMIT ?", + args: []interface{}{5}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results, err := QueryBerryData(tt.query, tt.args...) + + if (err != nil) != tt.expectError { + t.Errorf("QueryBerryData() error = %v, expectError %v", err, tt.expectError) + return + } + + if !tt.expectError && results == nil { + t.Error("QueryBerryData() should not return nil results on success") + } + }) + } +} + +func TestEmbeddedDBExists(t *testing.T) { + if len(embeddedDB) == 0 { + t.Error("embeddedDB should not be empty") + } + + if len(embeddedDB) < 16 { + t.Error("embeddedDB appears to be too small to be a valid SQLite database") + } + + sqliteHeader := "SQLite format 3" + if !strings.HasPrefix(string(embeddedDB[:len(sqliteHeader)]), sqliteHeader) { + t.Error("embeddedDB does not appear to be a valid SQLite database") + } +} + +func TestQueryBerryDataTempFileCleanup(t *testing.T) { + initialTempFiles := countTempFiles() + + query := "SELECT COUNT(*) FROM berries" + _, err := QueryBerryData(query) + + if err != nil { + t.Fatalf("QueryBerryData() error = %v", err) + } + + finalTempFiles := countTempFiles() + + if finalTempFiles > initialTempFiles { + t.Errorf("Temporary files not cleaned up properly. Before: %d, After: %d", initialTempFiles, finalTempFiles) + } +} + +func countTempFiles() int { + tempDir := os.TempDir() + entries, err := os.ReadDir(tempDir) + if err != nil { + return 0 + } + + count := 0 + for _, entry := range entries { + if strings.Contains(entry.Name(), "berries-") && strings.HasSuffix(entry.Name(), ".db") { + count++ + } + } + return count +} diff --git a/connections/db/berries.db b/connections/db/berries.db new file mode 100644 index 0000000..66aaf46 Binary files /dev/null and b/connections/db/berries.db differ diff --git a/connections/db/berry_data.sql b/connections/db/berry_data.sql new file mode 100644 index 0000000..478e69c --- /dev/null +++ b/connections/db/berry_data.sql @@ -0,0 +1,411 @@ +-- -- scripts/create_berries.sql +CREATE TABLE berries ( + id INTEGER PRIMARY KEY, + name TEXT NULL UNIQUE, + effect TEXT NULL, + firmness TEXT NULL, + growth_time INTEGER NULL, + max_harvest INTEGER NULL, + natural_gift_power INTEGER NULL, + natural_gift_type TEXT NULL, + size INTEGER NULL, + smoothness INTEGER NULL, + soil_dryness INTEGER NULL, + sprite_url TEXT NULL +); + +CREATE TABLE berry_flavors ( + berry_id INTEGER NOT NULL, + flavor_name TEXT NOT NULL, + potency INTEGER NOT NULL, + FOREIGN KEY (berry_id) REFERENCES berries(id) +); + +INSERT INTO berries VALUES + (1, 'cheri', 'When paralyzed, cures paralysis', 'soft', 3, 5, 60, 'fire', 20, 25, 15, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/cheri-berry.png'), + (2, 'chesto', 'When asleep, cures sleeping', 'super-hard', 3, 5, 60, 'water', 80, 25, 15, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/chesto-berry.png'), + (3, 'pecha', 'When poisoned, cures poison', 'very-soft', 3, 5, 60, 'electric', 40, 25, 15, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/pecha-berry.png'), + (4, 'rawst', 'When burned, cures burn', 'hard', 3, 5, 60, 'grass', 32, 25, 15, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/rawst-berry.png'), + (5, 'aspear', 'When frozen, cures freeze', 'super-hard', 3, 5, 60, 'ice', 50, 25, 15, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/aspear-berry.png'), + (6, 'leppa', 'When one move reaches 0 PP, restores 10 PP of the move', 'very-hard', 4, 5, 60, 'fighting', 28, 20, 15, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/leppa-berry.png'), + (7, 'oran', 'When holder has less than ½ of their max HP, restores 10 HP', 'super-hard', 4, 5, 60, 'poison', 35, 20, 15, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/oran-berry.png'), + (8, 'persim', 'When confused, cures confusion', 'hard', 4, 5, 60, 'ground', 47, 20, 15, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/persim-berry.png'), + (9, 'lum', 'When holder has a status effect or confusion, cures the status effect', 'super-hard', 12, 5, 60, 'flying', 34, 20, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/lum-berry.png'), + (10, 'sitrus', 'When holder has less than ½ of their max HP, restores ¼ of max HP', 'very-hard', 8, 5, 60, 'psychic', 95, 20, 7, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/sitrus-berry.png'), + (11, 'figy', 'When holder has less than ¼ of their max HP, restores ⅓ of max HP, but confuses the holder if it dislikes the spicy flavor', 'soft', 5, 5, 60, 'bug', 100, 25, 10, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/figy-berry.png'), + (12, 'wiki', 'When holder has less than ¼ of their max HP, restores ⅓ of max HP, but confuses the holder if it dislikes the dry flavor', 'hard', 5, 5, 60, 'rock', 115, 25, 10, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/wiki-berry.png'), + (13, 'mago', 'When holder has less than ¼ of their max HP, restores ⅓ of max HP, but confuses the holder if it dislikes the sweet flavor', 'hard', 5, 5, 60, 'ghost', 126, 25, 10, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/mago-berry.png'), + (14, 'aguav', 'When holder has less than ¼ of their max HP, restores ⅓ of max HP, but confuses the holder if it dislikes the bitter flavor', 'super-hard', 5, 5, 60, 'dragon', 64, 25, 10, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/aguav-berry.png'), + (15, 'iapapa', 'When holder has less than ¼ of their max HP, restores ⅓ of max HP, but confuses the holder if it dislikes the sour flavor', 'soft', 5, 5, 60, 'dark', 223, 25, 10, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/iapapa-berry.png'), + (16, 'razz', 'Makes wild Pokémon easier to catchPE', 'very-hard', 2, 10, 60, 'steel', 120, 20, 35, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/razz-berry.png'), + (17, 'bluk', 'No effect; only useful for planting and cooking.', 'soft', 2, 10, 70, 'fire', 108, 20, 35, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/bluk-berry.png'), + (18, 'nanab', 'No effect; only useful for planting and cooking.', 'very-hard', 2, 10, 70, 'water', 77, 20, 35, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/nanab-berry.png'), + (19, 'wepear', 'No effect; only useful for planting and cooking.', 'super-hard', 2, 10, 70, 'electric', 74, 20, 35, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/wepear-berry.png'), + (20, 'pinap', 'No effect; only useful for planting and cooking.', 'hard', 2, 10, 70, 'grass', 80, 20, 35, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/pinap-berry.png'), + (21, 'pomeg', 'Increases friendship but lowers HP EVs by 10', 'very-hard', 8, 5, 70, 'ice', 135, 20, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/pomeg-berry.png'), + (22, 'kelpsy', 'Increases friendship but lowers Attack EVs by 10', 'hard', 8, 5, 70, 'fighting', 150, 20, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/kelpsy-berry.png'), + (23, 'qualot', 'Increases friendship but lowers Defense EVs by 10', 'hard', 8, 5, 70, 'poison', 110, 20, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/qualot-berry.png'), + (24, 'hondew', 'Increases friendship but lowers Sp. Atk EVs by 10', 'hard', 8, 5, 70, 'ground', 162, 20, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/hondew-berry.png'), + (25, 'grepa', 'Increases friendship but lowers Sp. Def EVs by 10', 'soft', 8, 5, 70, 'flying', 149, 20, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/grepa-berry.png'), + (26, 'tamato', 'Increases friendship but lowers Speed EVs by 10', 'soft', 8, 5, 70, 'psychic', 200, 30, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/tamato-berry.png'), + (27, 'cornn', 'No effect; only useful for planting and cooking.', 'hard', 6, 10, 70, 'bug', 75, 30, 10, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/cornn-berry.png'), + (28, 'magost', 'No effect; only useful for planting and cooking.', 'hard', 6, 10, 70, 'rock', 140, 30, 10, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/magost-berry.png'), + (29, 'rabuta', 'No effect; only useful for planting and cooking.', 'soft', 6, 10, 70, 'ghost', 226, 30, 10, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/rabuta-berry.png'), + (30, 'nomel', 'No effect; only useful for planting and cooking.', 'super-hard', 6, 10, 70, 'dragon', 285, 30, 10, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/nomel-berry.png'), + (31, 'spelon', 'No effect; only useful for planting and cooking.', 'soft', 15, 15, 70, 'dark', 133, 35, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/spelon-berry.png'), + (32, 'pamtre', 'No effect; only useful for planting and cooking.', 'very-soft', 15, 15, 70, 'steel', 244, 35, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/pamtre-berry.png'), + (33, 'watmel', 'No effect; only useful for planting and cooking.', 'soft', 15, 15, 80, 'fire', 250, 35, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/watmel-berry.png'), + (34, 'durin', 'No effect; only useful for planting and cooking.', 'hard', 15, 15, 80, 'water', 280, 35, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/durin-berry.png'), + (35, 'belue', 'No effect; only useful for planting and cooking.', 'very-soft', 15, 15, 80, 'electric', 300, 35, 8, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/belue-berry.png'), + (36, 'occa', 'When holder is hit by a supereffective Fire-type move, halves the damage', 'super-hard', 18, 5, 60, 'fire', 90, 30, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/occa-berry.png'), + (37, 'passho', 'When holder is hit by a supereffective Water-type move, halves the damage', 'soft', 18, 5, 60, 'water', 33, 30, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/passho-berry.png'), + (38, 'wacan', 'When holder is hit by a supereffective Electric-type move, halves the damage', 'very-soft', 18, 5, 60, 'electric', 250, 30, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/wacan-berry.png'), + (39, 'rindo', 'When holder is hit by a supereffective Grass-type move, halves the damage', 'soft', 18, 5, 60, 'grass', 156, 30, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/rindo-berry.png'), + (40, 'yache', 'When holder is hit by a supereffective Ice-type move, halves the damage', 'very-hard', 18, 5, 60, 'ice', 135, 30, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/yache-berry.png'), + (41, 'chople', 'When holder is hit by a supereffective Fighting-type move, halves the damage', 'soft', 18, 5, 60, 'fighting', 77, 30, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/chople-berry.png'), + (42, 'kebia', 'When holder is hit by a supereffective Poison-type move, halves the damage', 'hard', 18, 5, 60, 'poison', 90, 30, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/kebia-berry.png'), + (43, 'shuca', 'When holder is hit by a supereffective Ground-type move, halves the damage', 'soft', 18, 5, 60, 'ground', 42, 30, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/shuca-berry.png'), + (44, 'coba', 'When holder is hit by a supereffective Flying-type move, halves the damage', 'very-hard', 18, 5, 60, 'flying', 278, 30, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/coba-berry.png'), + (45, 'payapa', 'When holder is hit by a supereffective Psychic-type move, halves the damage', 'soft', 18, 5, 60, 'psychic', 252, 30, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/payapa-berry.png'), + (46, 'tanga', 'When holder is hit by a supereffective Bug-type move, halves the damage', 'very-soft', 18, 5, 60, 'bug', 42, 35, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/tanga-berry.png'), + (47, 'charti', 'When holder is hit by a supereffective Rock-type move, halves the damage', 'very-soft', 18, 5, 60, 'rock', 28, 35, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/charti-berry.png'), + (48, 'kasib', 'When holder is hit by a supereffective Ghost-type move, halves the damage', 'hard', 18, 5, 60, 'ghost', 144, 35, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/kasib-berry.png'), + (49, 'haban', 'When holder is hit by a supereffective Dragon-type move, halves the damage', 'soft', 18, 5, 60, 'dragon', 23, 35, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/haban-berry.png'), + (50, 'colbur', 'When holder is hit by a supereffective Dark-type move, halves the damage', 'super-hard', 18, 5, 60, 'dark', 39, 35, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/colbur-berry.png'), + (51, 'babiri', 'When holder is hit by a supereffective Steel-type move, halves the damage', 'super-hard', 18, 5, 60, 'steel', 265, 35, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/babiri-berry.png'), + (52, 'chilan', 'When holder is hit by a Normal-type move, halves the damage', 'very-soft', 18, 5, 60, 'normal', 34, 35, 6, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/chilan-berry.png'), + (53, 'liechi', 'When holder has less than ¼ of their max HP, raises Attack', 'very-hard', 24, 5, 80, 'grass', 111, 40, 4, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/liechi-berry.png'), + (54, 'ganlon', 'When holder has less than ¼ of their max HP, raises Defense', 'very-hard', 24, 5, 80, 'ice', 33, 40, 4, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/ganlon-berry.png'), + (55, 'salac', 'When holder has less than ¼ of their max HP, raises Speed', 'very-hard', 24, 5, 80, 'fighting', 95, 40, 4, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/salac-berry.png'), + (56, 'petaya', 'When holder has less than ¼ of their max HP, raises Sp. Atk', 'very-hard', 24, 5, 80, 'poison', 237, 40, 4, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/petaya-berry.png'), + (57, 'apicot', 'When holder has less than ¼ of their max HP, raises Sp. Def', 'hard', 24, 5, 80, 'ground', 75, 40, 4, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/apicot-berry.png'), + (58, 'lansat', 'When holder has less than ¼ of their max HP, raises critical-hit ratio', 'soft', 24, 5, 80, 'flying', 97, 50, 4, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/lansat-berry.png'), + (59, 'starf', 'When holder has less than ¼ of their max HP, raises a random stat', 'super-hard', 24, 5, 80, 'psychic', 153, 50, 4, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/starf-berry.png'), + (60, 'enigma', 'When holder is hit by a supereffective move, raises ¼ of their max HP', 'hard', 24, 5, 80, 'bug', 155, 60, 7, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/enigma-berry.png'), + (61, 'micle', 'When holder has less than ¼ their max HP, raises accuracy of next move', 'soft', 24, 5, 80, 'rock', 41, 60, 7, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/micle-berry.png'), + (62, 'custap', 'When holder has less than ¼ their max HP, holder will go first for the next turn', 'super-hard', 24, 5, 80, 'ghost', 267, 60, 7, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/custap-berry.png'), + (63, 'jaboca', 'When holder is hit by a physical move, attacker is also hurt', 'soft', 24, 5, 80, 'dragon', 33, 60, 7, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/jaboca-berry.png'), + (64, 'rowap', 'When holder is hit by a special move, attacker is also hurt', 'very-soft', 24, 5, 80, 'dark', 52, 60, 7, 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/rowap-berry.png'); + +-- Berry flavors data +INSERT INTO berry_flavors VALUES + (1, 'spicy', 10), + (1, 'dry', 0), + (1, 'sweet', 0), + (1, 'bitter', 0), + (1, 'sour', 0), + (2, 'spicy', 0), + (2, 'dry', 10), + (2, 'sweet', 0), + (2, 'bitter', 0), + (2, 'sour', 0), + (3, 'spicy', 0), + (3, 'dry', 0), + (3, 'sweet', 10), + (3, 'bitter', 0), + (3, 'sour', 0), + (4, 'spicy', 0), + (4, 'dry', 0), + (4, 'sweet', 0), + (4, 'bitter', 10), + (4, 'sour', 0), + (5, 'spicy', 0), + (5, 'dry', 0), + (5, 'sweet', 0), + (5, 'bitter', 0), + (5, 'sour', 10), + (6, 'spicy', 10), + (6, 'dry', 0), + (6, 'sweet', 10), + (6, 'bitter', 10), + (6, 'sour', 10), + (7, 'spicy', 10), + (7, 'dry', 10), + (7, 'sweet', 0), + (7, 'bitter', 10), + (7, 'sour', 10), + (8, 'spicy', 10), + (8, 'dry', 10), + (8, 'sweet', 10), + (8, 'bitter', 0), + (8, 'sour', 10), + (9, 'spicy', 10), + (9, 'dry', 10), + (9, 'sweet', 10), + (9, 'bitter', 10), + (9, 'sour', 0), + (10, 'spicy', 0), + (10, 'dry', 10), + (10, 'sweet', 10), + (10, 'bitter', 10), + (10, 'sour', 10), + (11, 'spicy', 15), + (11, 'dry', 0), + (11, 'sweet', 0), + (11, 'bitter', 0), + (11, 'sour', 0), + (12, 'spicy', 0), + (12, 'dry', 15), + (12, 'sweet', 0), + (12, 'bitter', 0), + (12, 'sour', 0), + (13, 'spicy', 0), + (13, 'dry', 0), + (13, 'sweet', 15), + (13, 'bitter', 0), + (13, 'sour', 0), + (14, 'spicy', 0), + (14, 'dry', 0), + (14, 'sweet', 0), + (14, 'bitter', 15), + (14, 'sour', 0), + (15, 'spicy', 0), + (15, 'dry', 0), + (15, 'sweet', 0), + (15, 'bitter', 0), + (15, 'sour', 15), + (16, 'spicy', 10), + (16, 'dry', 10), + (16, 'sweet', 0), + (16, 'bitter', 0), + (16, 'sour', 0), + (17, 'spicy', 0), + (17, 'dry', 10), + (17, 'sweet', 10), + (17, 'bitter', 0), + (17, 'sour', 0), + (18, 'spicy', 0), + (18, 'dry', 0), + (18, 'sweet', 10), + (18, 'bitter', 10), + (18, 'sour', 0), + (19, 'spicy', 0), + (19, 'dry', 0), + (19, 'sweet', 0), + (19, 'bitter', 10), + (19, 'sour', 10), + (20, 'spicy', 10), + (20, 'dry', 0), + (20, 'sweet', 0), + (20, 'bitter', 0), + (20, 'sour', 10), + (21, 'spicy', 10), + (21, 'dry', 0), + (21, 'sweet', 10), + (21, 'bitter', 10), + (21, 'sour', 0), + (22, 'spicy', 0), + (22, 'dry', 10), + (22, 'sweet', 0), + (22, 'bitter', 10), + (22, 'sour', 10), + (23, 'spicy', 10), + (23, 'dry', 0), + (23, 'sweet', 10), + (23, 'bitter', 0), + (23, 'sour', 10), + (24, 'spicy', 10), + (24, 'dry', 10), + (24, 'sweet', 0), + (24, 'bitter', 10), + (24, 'sour', 0), + (25, 'spicy', 0), + (25, 'dry', 10), + (25, 'sweet', 10), + (25, 'bitter', 0), + (25, 'sour', 10), + (26, 'spicy', 20), + (26, 'dry', 10), + (26, 'sweet', 0), + (26, 'bitter', 0), + (26, 'sour', 0), + (27, 'spicy', 0), + (27, 'dry', 20), + (27, 'sweet', 10), + (27, 'bitter', 0), + (27, 'sour', 0), + (28, 'spicy', 0), + (28, 'dry', 0), + (28, 'sweet', 20), + (28, 'bitter', 10), + (28, 'sour', 0), + (29, 'spicy', 0), + (29, 'dry', 0), + (29, 'sweet', 0), + (29, 'bitter', 20), + (29, 'sour', 10), + (30, 'spicy', 10), + (30, 'dry', 0), + (30, 'sweet', 0), + (30, 'bitter', 0), + (30, 'sour', 20), + (31, 'spicy', 30), + (31, 'dry', 10), + (31, 'sweet', 0), + (31, 'bitter', 0), + (31, 'sour', 0), + (32, 'spicy', 0), + (32, 'dry', 30), + (32, 'sweet', 10), + (32, 'bitter', 0), + (32, 'sour', 0), + (33, 'spicy', 0), + (33, 'dry', 0), + (33, 'sweet', 30), + (33, 'bitter', 10), + (33, 'sour', 0), + (34, 'spicy', 0), + (34, 'dry', 0), + (34, 'sweet', 0), + (34, 'bitter', 30), + (34, 'sour', 10), + (35, 'spicy', 10), + (35, 'dry', 0), + (35, 'sweet', 0), + (35, 'bitter', 0), + (35, 'sour', 30), + (36, 'spicy', 15), + (36, 'dry', 0), + (36, 'sweet', 10), + (36, 'bitter', 0), + (36, 'sour', 0), + (37, 'spicy', 0), + (37, 'dry', 15), + (37, 'sweet', 0), + (37, 'bitter', 10), + (37, 'sour', 0), + (38, 'spicy', 0), + (38, 'dry', 0), + (38, 'sweet', 15), + (38, 'bitter', 0), + (38, 'sour', 10), + (39, 'spicy', 10), + (39, 'dry', 0), + (39, 'sweet', 0), + (39, 'bitter', 15), + (39, 'sour', 0), + (40, 'spicy', 0), + (40, 'dry', 10), + (40, 'sweet', 0), + (40, 'bitter', 0), + (40, 'sour', 15), + (41, 'spicy', 15), + (41, 'dry', 0), + (41, 'sweet', 0), + (41, 'bitter', 10), + (41, 'sour', 0), + (42, 'spicy', 0), + (42, 'dry', 15), + (42, 'sweet', 0), + (42, 'bitter', 0), + (42, 'sour', 10), + (43, 'spicy', 10), + (43, 'dry', 0), + (43, 'sweet', 15), + (43, 'bitter', 0), + (43, 'sour', 0), + (44, 'spicy', 0), + (44, 'dry', 10), + (44, 'sweet', 0), + (44, 'bitter', 15), + (44, 'sour', 0), + (45, 'spicy', 0), + (45, 'dry', 0), + (45, 'sweet', 10), + (45, 'bitter', 0), + (45, 'sour', 15), + (46, 'spicy', 20), + (46, 'dry', 0), + (46, 'sweet', 0), + (46, 'bitter', 0), + (46, 'sour', 10), + (47, 'spicy', 10), + (47, 'dry', 20), + (47, 'sweet', 0), + (47, 'bitter', 0), + (47, 'sour', 0), + (48, 'spicy', 0), + (48, 'dry', 10), + (48, 'sweet', 20), + (48, 'bitter', 0), + (48, 'sour', 0), + (49, 'spicy', 0), + (49, 'dry', 0), + (49, 'sweet', 10), + (49, 'bitter', 20), + (49, 'sour', 0), + (50, 'spicy', 0), + (50, 'dry', 0), + (50, 'sweet', 0), + (50, 'bitter', 10), + (50, 'sour', 20), + (51, 'spicy', 25), + (51, 'dry', 10), + (51, 'sweet', 0), + (51, 'bitter', 0), + (51, 'sour', 0), + (52, 'spicy', 0), + (52, 'dry', 25), + (52, 'sweet', 10), + (52, 'bitter', 0), + (52, 'sour', 0), + (53, 'spicy', 30), + (53, 'dry', 10), + (53, 'sweet', 30), + (53, 'bitter', 0), + (53, 'sour', 0), + (54, 'spicy', 0), + (54, 'dry', 30), + (54, 'sweet', 10), + (54, 'bitter', 30), + (54, 'sour', 0), + (55, 'spicy', 0), + (55, 'dry', 0), + (55, 'sweet', 30), + (55, 'bitter', 10), + (55, 'sour', 30), + (56, 'spicy', 30), + (56, 'dry', 0), + (56, 'sweet', 0), + (56, 'bitter', 30), + (56, 'sour', 10), + (57, 'spicy', 10), + (57, 'dry', 30), + (57, 'sweet', 0), + (57, 'bitter', 0), + (57, 'sour', 30), + (58, 'spicy', 30), + (58, 'dry', 10), + (58, 'sweet', 30), + (58, 'bitter', 10), + (58, 'sour', 30), + (59, 'spicy', 30), + (59, 'dry', 10), + (59, 'sweet', 30), + (59, 'bitter', 10), + (59, 'sour', 30), + (60, 'spicy', 40), + (60, 'dry', 10), + (60, 'sweet', 0), + (60, 'bitter', 0), + (60, 'sour', 0), + (61, 'spicy', 0), + (61, 'dry', 40), + (61, 'sweet', 10), + (61, 'bitter', 0), + (61, 'sour', 0), + (62, 'spicy', 0), + (62, 'dry', 0), + (62, 'sweet', 40), + (62, 'bitter', 10), + (62, 'sour', 0), + (63, 'spicy', 0), + (63, 'dry', 0), + (63, 'sweet', 0), + (63, 'bitter', 40), + (63, 'sour', 10), + (64, 'spicy', 10), + (64, 'dry', 0), + (64, 'sweet', 0), + (64, 'bitter', 0), + (64, 'sour', 40); diff --git a/docs/Infrastructure_Guide/aws.md b/docs/Infrastructure_Guide/aws.md index f94a510..70ce4e9 100644 --- a/docs/Infrastructure_Guide/aws.md +++ b/docs/Infrastructure_Guide/aws.md @@ -3,11 +3,29 @@ weight: 2 --- # 2. AWS +Amazon Web Services was the chosen cloud vendor for hosting this project's infrastructure. -Amazon Web Services was the chosen cloud vendor for hosting this project's infrastructure. -This page will describe how to create each resource manually first to get used to the console. Then, in [3. Terraform](terraform.md), -IaC (Infrastructure as Code) files will be created so that all resources can better managed and easily destroyed or rebuilt. +!!! question "What is AWS?" + + AWS (Amazon Web Services) is a cloud platform that gives you on-demand access to things like computing + power, storage, and databases, along with a wide variety of other services. Instead of setting up and + maintaining physical servers, you can use AWS to quickly build, deploy, and scale applications of all + sizes. From hosting websites to running machine learning models, AWS provides flexible tools to support + different kinds of projects, with built-in options for security, monitoring, and global reach. + + View more [about AWS](https://aws.amazon.com/what-is-aws/) + +## Services Used +* [IAM](#iam) +* [VPC](#vpc) +* [RDS](#rds) + +!!! note + + The instructions below are all focused on creating AWS resources through the web console (can be helpful if new to AWS to learn how to + navigate the console) . Since this project uses Terraform, all resources can be created and destroyed through IaC. Refer to the + [Terraform](terraform.md) page to create the resources through Terraform. --- @@ -35,14 +53,48 @@ This would make more sense if there were several users or different projects und ### Setup Instructions 1. Visit the IAM Console. -2. - +2. On the left, under **Access Management**, click on **Users**. +3. Click on the **Create User** button in the upper-right. +4. Provide a name for the user. Ideally, the name should reflect the role or service it'll work with. +5. Click **next**. +6. Choose to **attack policies directly** +7. In the **Permission Policy** section, the option to attach an existing AWS managed policy or create a custom one exists. + * AWS Managed Policies + * Depending on what the user account is being created for, an existing AWS managed policy could suffice. + * For example, this project's `elastic-container-registry-user` account has the AWS managed `SecretsManagerReadWrite` policy that + allows it to read and write secrets from/to [Secret Manager](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/SecretsManagerReadWrite.html). + * Custom Policies + * For even more fine-grain control and granting [least-privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege), [custom](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create.html) or _customer managed_ policies can be created. + * For example, this project's `terraform-user` account has a policy that grants access to describe resources with in the EC2 service: + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "TerraformerRDSPermissions", + "Effect": "Allow", + "Action": [ + "ec2:DescribeVpcAttribute", + "ec2:DescribeVpcs", + "ec2:DescribeRouteTables", + "ec2:DescribeSubnets", + "ec2:DescribeInternetGateways", + "ec2:DescribeSecurityGroups", + "ec2:DescribeNatGateways", + "ec2:DescribeVpcEndpoints" + ], + "Resource": "*" + } + ] + } + ``` +8. ... --- ## VPC _Virtual Private Cloud_ -Creating a VPC should be one of the initial services to configure so that it's available for selection when setting up other services later on. +Creating a custom VPC instead of using the default one provides full control over network configuration, security, and isolation tailored to specific application requirements. At first, the VPC will have public subnets to test the local version of Dagster to make sure everything is working correctly. The VPC will then be modified to only have private subnet groups. @@ -67,9 +119,19 @@ AWS creates a default VPC, but learning to create one can be invaluable when nee ## RDS _Relational Database Service_ -1. Choose PostgreSQL -2. Choose dev/test -3. Single zone -4. Burstable class -5. t4g.micro instance -6. Change storage to 20GB \ No newline at end of file +Amazon RDS is a managed service that simplifies the setup, operation, and scaling of relational databases in the cloud. +In this project, [PostgreSQL](https://www.postgresql.org/) is the database engine of choice for storing the metadata of Dagster. + +The cost to maintain the database with the project's configuration options come out to ~$15.00 USD. + +### Setup Instructions + +1. Visit the [RDS console](https://console.aws.amazon.com/rds/home). +2. On the **dashboard**, there should be an option **Create a Database**. If not, click on **Databases** on the left menu. + Then click **Create Database** in the upper-right. +3. Choose PostgreSQL +4. Choose dev/test +5. Single zone +6. Burstable class +7. t4g.micro instance +8. Change storage to 20GB \ No newline at end of file diff --git a/docs/Infrastructure_Guide/dagster.md b/docs/Infrastructure_Guide/dagster.md index a8ea17a..192c20a 100644 --- a/docs/Infrastructure_Guide/dagster.md +++ b/docs/Infrastructure_Guide/dagster.md @@ -1,8 +1,8 @@ --- -weight: 5 +weight: 6 --- -# 5. Dagster +# 6. Dagster !!! question "What is Dagster?" @@ -13,9 +13,41 @@ weight: 5 View more [about Dagster](https://dagster.io/platform-overview) ## Installation -Dagster and its components can be installed with `uv`: ` +Dagster and its components needed for the project can be installed with `uv`: ` ```bash -uv add dagster dagster-webserver dagster-dg-cli dagster-postgres>=0.27.3 +uv add dagster dagster-webserver dagster-dg-cli dagster-postgres>=0.27.3 dagster-dbt +``` + +## Project Layout +In my experience, Dagster needed a specific directory structure in order for the program to find all necessary files. +This project uses a directory named `pipelines` to store all the Dagster files: + +``` +. +└── pipelines/ + ├── defs/ + │ ├── extract/ + │ │ └── extract_data.py + │ ├── load/ + │ │ └── load_data.py + │ └── transformation/ + │ └── transform_data.py + ├── poke_cli_dbt/ + │ ├── logs + │ ├── macros/ + │ │ ├── create_relationships.sql + │ │ └── create_rls.sql + │ ├── models/ + │ │ ├── cards.sql + │ │ ├── series.sql + │ │ ├── sets.sql + │ │ └── sources.yml + │ ├── target + │ ├── dbt_project.yml + │ └── profiles.yml + └── soda/ + ├── checks.yml + └── configuration.yml ``` diff --git a/docs/Infrastructure_Guide/dbt.md b/docs/Infrastructure_Guide/dbt.md index 515cb1f..4cd17ec 100644 --- a/docs/Infrastructure_Guide/dbt.md +++ b/docs/Infrastructure_Guide/dbt.md @@ -14,8 +14,48 @@ weight: 5 View more [about dbt](www.getdbt.com/product/what-is-dbt) -## Installation +## Installation & Initialization + +Install with `uv`: +```bash +uv add dbt +``` + +Initialize a `dbt` project in the `card_data` directory: +```bash +dbt init +``` + +Follow the prompts to finish setting up the `dbt` project. + +## Models +Models are the pieces of SQL code that run when using that `dbt build` command that _build_ the +tables to the destination schema. In this project, that would the `public` schema in the PostgreSQL +database on Supabase. + +The `public` schema is the public facing schema that exposes the API to the data in the tables. ## Sources -* [docs](https://docs.getdbt.com/docs/build/sources) -Create a `source.yml` file under the `models/` directory +Create a `source.yml` file under the `models/` directory. More info on [sources here](https://docs.getdbt.com/docs/build/sources). + +This file is used to declare and configure the raw data sources. These tables are the foundation for +the dbt models but are not managed by dbt itself. + +For example: +```yaml +sources: + - name: staging + description: "Staging schema containing raw data loaded from extract pipeline" + tables: + - name: series + description: "Pokemon card series data" + columns: + - name: id + description: "Unique series identifier" + - name: name + description: "Series name" + - name: logo + description: "Series logo URL" +``` + +The above `yml` defines the structure for the raw `series` table from the `staging` schema. \ No newline at end of file diff --git a/docs/Infrastructure_Guide/index.md b/docs/Infrastructure_Guide/index.md index 75af52d..16290f1 100644 --- a/docs/Infrastructure_Guide/index.md +++ b/docs/Infrastructure_Guide/index.md @@ -4,7 +4,7 @@ weight: 1 # 1. Overview -This section serves as a knowledge base for the project’s backend infrastructure, created for several purposes: +This section serves as a knowledge base for the project’s backend infrastructure. It was created for a few purposes: 1. To document how I built everything, so I can easily reference it later. 2. To help others learn how to build something similar. @@ -38,7 +38,7 @@ Below is a list of all the tools and services used in this project's infrastruct !!! note Keep in mind that the purpose of this project is to explore and learn new tools, services, and programming languages. - Some design choices might be overkill, included purely for learning, or might not make much sense at all. + Some design choices might be overkill or not make much sense at all, but included purely for learning. I'm not an expert! If you notice anything strange or think something could be improved, please feel free to open a [GitHub Issue](https://github.com/digitalghost-dev/poke-cli/issues) and offer a suggestion. \ No newline at end of file diff --git a/docs/Infrastructure_Guide/python.md b/docs/Infrastructure_Guide/python.md new file mode 100644 index 0000000..d68b68e --- /dev/null +++ b/docs/Infrastructure_Guide/python.md @@ -0,0 +1,23 @@ +--- +weight: 4 +--- + +# 4. Python + +## Installing uv +_uv is the main package and project manager used in this project._ + +Learn more about [uv](https://docs.astral.sh/uv/). + +1. Install via their [installation script](https://docs.astral.sh/uv/getting-started/installation/): + ```bash + curl -LsSf https://astral.sh/uv/install.sh | sh + ``` + or brew: + ```bash + brew install uv + ``` +2. Install Python with `uv`: + ```bash + uv python install 3.12 + ``` \ No newline at end of file diff --git a/go.mod b/go.mod index ba208e4..ae183f2 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/google/uuid v1.6.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -35,13 +36,20 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/image v0.28.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.34.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.39.0 // indirect ) // v1.3.4 was pushed as test and not a real version. diff --git a/go.sum b/go.sum index 765af5d..688379a 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -64,8 +66,12 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -75,6 +81,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= @@ -91,3 +99,11 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY= +modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= diff --git a/nfpm.yaml b/nfpm.yaml index 83f76d1..f950d36 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -1,7 +1,7 @@ name: "poke-cli" arch: "arm64" platform: "linux" -version: "v1.6.2" +version: "v1.7.0" section: "default" version_schema: semver maintainer: "Christian S" diff --git a/structs/structs.go b/structs/structs.go index 92b41ad..df484da 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -35,6 +35,28 @@ type AbilityJSONStruct struct { } `json:"pokemon"` } +// Berry represents a berry from the local SQLite db +type Berry struct { + ID int `db:"id"` + Name string `db:"name"` + Effect string `db:"effect"` + Firmness string `db:"firmness"` + GrowthTime int `db:"growth_time"` + MaxHarvest int `db:"max_harvest"` + NaturalGiftPower int `db:"natural_gift_power"` + NaturalGiftType string `db:"natural_gift_type"` + Size int `db:"size"` + Smoothness int `db:"smoothness"` + SoilDryness int `db:"soil_dryness"` +} + +// BerryFlavor represents berry flavor data from the local SQLite db +type BerryFlavor struct { + BerryID int `db:"berry_id"` + FlavorName string `db:"flavor_name"` + Potency int `db:"potency"` +} + // ItemJSONStruct item endpoint from API type ItemJSONStruct struct { Name string `json:"name"` diff --git a/testdata/cli_help.golden b/testdata/cli_help.golden index 3076d2f..f2840b8 100644 --- a/testdata/cli_help.golden +++ b/testdata/cli_help.golden @@ -13,6 +13,7 @@ │ │ │ COMMANDS: │ │ ability Get details about an ability │ +│ berry Get details about a berry │ │ item Get details about an item │ │ move Get details about a move │ │ natures Get details about all natures │ diff --git a/testdata/cli_incorrect_command.golden b/testdata/cli_incorrect_command.golden index 200015f..e57f2eb 100644 --- a/testdata/cli_incorrect_command.golden +++ b/testdata/cli_incorrect_command.golden @@ -4,6 +4,7 @@ │ │ │Commands: │ │ ability Get details about an ability │ +│ berry Get details about a berry │ │ item Get details about an item │ │ move Get details about a move │ │ natures Get details about all natures │ diff --git a/testdata/main_latest_flag.golden b/testdata/main_latest_flag.golden index a362291..9905677 100644 --- a/testdata/main_latest_flag.golden +++ b/testdata/main_latest_flag.golden @@ -1,6 +1,6 @@ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ ┃ ┃ Latest available version: ┃ -┃ • v1.6.1 ┃ +┃ • v1.6.2 ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛