From 8ac4d6345299c317c2631024b9f42c5c4afce6d8 Mon Sep 17 00:00:00 2001 From: Cody Couperus Date: Sun, 1 Feb 2026 18:57:40 -0800 Subject: [PATCH] ready set go --- .gitignore | 1 + go.mod | 5 ++ go.sum | 2 + main.go | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..656c433 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gitea.coupxd.com/cody/go-weather + +go 1.25.6 + +require github.com/lib/pq v1.11.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3ad08b1 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI= +github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..900c5cd --- /dev/null +++ b/main.go @@ -0,0 +1,235 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" + + _ "github.com/lib/pq" +) + +type WeatherResponse struct { + Name string `json:"name"` + Main struct { + Temp float64 `json:"temp"` + FeelsLike float64 `json:"feels_like"` + Humidity int `json:"humidity"` + } + + Weather []struct { + Description string `json:"description"` + } `json:"weather"` +} + +type Reading struct { + ID int `json:"id"` + City string `json:"city"` + Temp float64 `json:"temp"` + FeelsLike float64 `json:"feels_like"` + Humidity int `json:"humidity"` + Conditions string `json:"conditions"` + FetchedAt time.Time `json:"fetched_at"` +} + +func getEnv(key string) string { + val := os.Getenv(key) + if val == "" { + log.Fatalf("missing required env var: %s", key) + } + return val +} + +func connectDB() *sql.DB { + connStr := fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + getEnv("DB_HOST"), + getEnv("DB_PORT"), + getEnv("DB_USER"), + getEnv("DB_PASSWORD"), + getEnv("DB_NAME"), + ) + + db, err := sql.Open("postgres", connStr) + if err != nil { + log.Fatalf("failed to connect to db: %v", err) + } + + err = db.Ping() + if err != nil { + log.Fatalf("failed to connect to db: %v", err) + } + + fmt.Println("connected to PostgreSQL") + return db +} + +func createTable(db *sql.DB) { + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS weather_readings ( + id SERIAL PRIMARY KEY, + city TEXT NOT NULL, + temp DOUBLE PRECISION NOT NULL, + feels_like DOUBLE PRECISION NOT NULL, + humidity INTEGER NOT NULL, + conditions TEXT NOT NULL, + fetched_at TIMESTAMP WITH TIME ZONE NOT NULL + ) + `) + + if err != nil { + log.Fatalf("failed to create table: %v", err) + } + fmt.Println("weather_readings table ready") +} + +func saveReading(db *sql.DB, weather WeatherResponse) (int, error) { + conditions := "" + if len(weather.Weather) > 0 { + conditions = weather.Weather[0].Description + } + + var id int + err := db.QueryRow( + `INSERT INTO weather_readings (city, temp, feels_like, humidity, conditions, fetched_at) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id`, + weather.Name, + weather.Main.Temp, + weather.Main.FeelsLike, + weather.Main.Humidity, + conditions, + time.Now(), + ).Scan(&id) + if err != nil { + return 0, fmt.Errorf("failed to save reading: %w", err) + } + return id, nil +} + +func getReadings(db *sql.DB, city string, limit int) ([]Reading, error) { + rows, err := db.Query( + `SELECT id, city, temp, feels_like, humidity, conditions, fetched_at + FROM weather_readings + WHERE city ILIKE $1 + ORDER BY fetched_at DESC + LIMIT $2`, + city, limit, + ) + + if err != nil { + return nil, fmt.Errorf("Query failed: %w", err) + } + defer rows.Close() + + var readings []Reading + for rows.Next() { + var r Reading + err := rows.Scan(&r.ID, &r.City, &r.Temp, &r.FeelsLike, &r.Humidity, &r.Conditions, &r.FetchedAt) + + if err != nil { + return nil, fmt.Errorf("scan failed: %w", err) + } + + readings = append(readings, r) + } + return readings, nil +} + +func fetchWeather(apiKey, city string) (WeatherResponse, error) { + url := fmt.Sprintf( + "https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=imperial", + city, apiKey, + ) + resp, err := http.Get(url) + + if err != nil { + return WeatherResponse{}, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return WeatherResponse{}, fmt.Errorf("API returned status %d", resp.StatusCode) + } + + var weather WeatherResponse + err = json.NewDecoder(resp.Body).Decode(&weather) + if err != nil { + return WeatherResponse{}, fmt.Errorf("failed to parse response: %w", err) + } + return weather, nil +} + +func main() { + apiKey := getEnv("OPENWEATHERMAP_API_KEY") + defaultCity := getEnv("WEATHER_CITY") + + db := connectDB() + defer db.Close() + + createTable(db) + + // POST /weather/fetch?city=London - fetch current weather and save it + http.HandleFunc("/weather/fetch", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + city := r.URL.Query().Get("city") + if city == "" { + city = defaultCity + } + + weather, err := fetchWeather(apiKey, city) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + + // note closure + id, err := saveReading(db, weather) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "weather fetched and saved", + "id": id, + "city": weather.Name, + "temp": weather.Main.Temp, + }) + }) + + // GET /weather/history?city=London&limit=10 + http.HandleFunc("/weather/history", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } + + city := r.URL.Query().Get("city") + if city == "" { + city = defaultCity + } + + // note closure + readings, err := getReadings(db, city, 25) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(readings) + }) + + port := ":8080" + fmt.Printf("server listening on %s\n", port) + log.Fatal(http.ListenAndServe(port, nil)) + +}