feat: Add caching

Implements: https://todo.sr.ht/~timharek/yr/1
Signed-off-by: Tim Hårek Andreassen <tim@harek.no>
This commit is contained in:
Tim Hårek Andreassen 2024-10-04 23:20:26 +02:00
parent cb00a14019
commit 437f7f5f9a
No known key found for this signature in database
GPG key ID: E59C7734F0E10EB5
6 changed files with 126 additions and 15 deletions

View file

@ -13,15 +13,18 @@ import (
) )
type forecastH struct { type forecastH struct {
isJson bool isJson bool
isUTC bool isDebug bool
isWeb bool isUTC bool
f *yr.ForecastResult isWeb bool
f *yr.ForecastResult
} }
func forecastHelper(cmd *cobra.Command, args []string) *forecastH { func forecastHelper(cmd *cobra.Command, args []string) *forecastH {
isJson, err := cmd.Flags().GetBool(flags.JSON) isJson, err := cmd.Flags().GetBool(flags.JSON)
cobra.CheckErr(err) cobra.CheckErr(err)
isDebug, err := cmd.Flags().GetBool(flags.DEBUG)
cobra.CheckErr(err)
isUTC, err := cmd.Flags().GetBool(flags.UTC) isUTC, err := cmd.Flags().GetBool(flags.UTC)
cobra.CheckErr(err) cobra.CheckErr(err)
isWeb, err := cmd.Flags().GetBool(flags.WEB) isWeb, err := cmd.Flags().GetBool(flags.WEB)
@ -46,8 +49,15 @@ func forecastHelper(cmd *cobra.Command, args []string) *forecastH {
cobra.CheckErr(err) cobra.CheckErr(err)
} }
if isDebug {
fmt.Printf("Expires: %v\n", f.Expires)
fmt.Printf("LastModified: %v\n", f.LastModified)
fmt.Printf("Default cache-dir: %v\n", os.TempDir())
}
return &forecastH{ return &forecastH{
isJson, isJson,
isDebug,
isUTC, isUTC,
isWeb, isWeb,
f, f,

11
pkg/cache/cache.go vendored
View file

@ -7,6 +7,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"time"
) )
type Cache[T any] struct { type Cache[T any] struct {
@ -67,3 +68,13 @@ func (c *Cache[T]) Get(name string) (*T, error) {
return &result, nil return &result, nil
} }
// Returns if t is expired
func IsExpired(expire time.Time, now *time.Time) bool {
if now == nil {
tmp := time.Now()
now = &tmp
}
return now.Unix() >= expire.Unix()
}

View file

@ -3,6 +3,7 @@ package cache
import ( import (
"os" "os"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -25,3 +26,27 @@ func TestCache(t *testing.T) {
assert.Equal("test1", result.Prop1) assert.Equal("test1", result.Prop1)
assert.Equal("test2", result.Prop2) assert.Equal("test2", result.Prop2)
} }
func TestIsExpired(t *testing.T) {
layout := "2006-01-02 15:04:05"
assert := assert.New(t)
expirationTime, err := time.Parse(layout, "2024-10-04 22:22:00")
assert.NoError(err)
now, err := time.Parse(layout, "2024-10-04 22:21:00")
assert.NoError(err)
result := IsExpired(expirationTime, &now)
assert.False(result)
now, err = time.Parse(layout, "2024-10-04 22:22:00")
assert.NoError(err)
result = IsExpired(expirationTime, &now)
assert.True(result)
now, err = time.Parse(layout, "2024-10-04 22:23:00")
assert.NoError(err)
result = IsExpired(expirationTime, &now)
assert.True(result)
}

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"time"
) )
type Met struct { type Met struct {
@ -53,11 +54,32 @@ func (m *Met) Forecast(lat, lon float64, alt *int) (*LocationForecastResult, err
return nil, err return nil, err
} }
forecast := LocationForecastResult{} expiresRaw, ok := resp.Header["Expires"]
if !ok {
return nil, fmt.Errorf("Unable to get 'Expires' header")
}
expires, err := time.Parse(time.RFC1123, expiresRaw[0])
if err != nil {
return nil, fmt.Errorf("Unable to parse `expiresRaw`: %w", err)
}
lastModifiedRaw, ok := resp.Header["Last-Modified"]
if !ok {
return nil, fmt.Errorf("Unable to get 'Last-Modified' header")
}
lastModified, err := time.Parse(time.RFC1123, lastModifiedRaw[0])
if err != nil {
return nil, fmt.Errorf("Unable to parse `Last-Modified`: %w", err)
}
forecast := LocationForecast{}
err = json.Unmarshal(body, &forecast) err = json.Unmarshal(body, &forecast)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &forecast, nil return &LocationForecastResult{
Expires: expires,
LastModified: lastModified,
LocationForecast: forecast,
}, nil
} }

View file

@ -2,12 +2,13 @@ package met
import "time" import "time"
type cacheInfo struct { type LocationForecastResult struct {
Expires time.Time `json:"expires"` Expires time.Time `json:"expires"`
LastModified time.Time `json:"lastModified"` LastModified time.Time `json:"lastModified"`
LocationForecast
} }
type LocationForecastResult struct { type LocationForecast struct {
Type string `json:"type"` Type string `json:"type"`
Geometry struct { Geometry struct {

View file

@ -6,14 +6,16 @@ import (
"slices" "slices"
"time" "time"
"git.sr.ht/~timharek/yr/pkg/cache"
"git.sr.ht/~timharek/yr/pkg/met" "git.sr.ht/~timharek/yr/pkg/met"
"git.sr.ht/~timharek/yr/pkg/nominatim" "git.sr.ht/~timharek/yr/pkg/nominatim"
"git.sr.ht/~timharek/yr/yr/direction" "git.sr.ht/~timharek/yr/yr/direction"
) )
type Client struct { type Client struct {
met met.Met met met.Met
nom nominatim.Nominatim nom nominatim.Nominatim
cache cache.Cache[ForecastResult]
} }
func New() (*Client, error) { func New() (*Client, error) {
@ -26,8 +28,9 @@ func New() (*Client, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
cache := cache.New[ForecastResult]("")
return &Client{met: *met, nom: *nom}, nil return &Client{met: *met, nom: *nom, cache: *cache}, nil
} }
type wind struct { type wind struct {
@ -74,11 +77,21 @@ type Forecast struct {
} }
type ForecastResult struct { type ForecastResult struct {
Coordinates nominatim.Coordinates `json:"coordinates"` Expires time.Time `json:"expires"`
Forecast []Forecast `json:"forecast"` LastModified time.Time `json:"lastModified"`
Coordinates nominatim.Coordinates `json:"coordinates"`
Forecast []Forecast `json:"forecast"`
} }
func (c *Client) Now(q string) (*ForecastResult, error) { func (c *Client) Now(q string) (*ForecastResult, error) {
cacheResult, err := c.cache.Get(q)
if err != nil {
return nil, err
}
if cacheResult != nil && !cache.IsExpired(cacheResult.Expires, nil) {
return cacheResult, nil
}
coords, err := c.nom.Lookup(q) coords, err := c.nom.Lookup(q)
if err != nil { if err != nil {
return nil, err return nil, err
@ -102,6 +115,14 @@ func (c *Client) NowCoords(coords *nominatim.Coordinates, location *string) (*Fo
} }
func (c *Client) Forecast(q string) (*ForecastResult, error) { func (c *Client) Forecast(q string) (*ForecastResult, error) {
cacheResult, err := c.cache.Get(q)
if err != nil {
return nil, err
}
if cacheResult != nil && !cache.IsExpired(cacheResult.Expires, nil) {
return cacheResult, nil
}
coords, err := c.nom.Lookup(q) coords, err := c.nom.Lookup(q)
if err != nil { if err != nil {
return nil, err return nil, err
@ -114,6 +135,14 @@ func (c *Client) Forecast(q string) (*ForecastResult, error) {
} }
func (c *Client) ForecastCoords(coords *nominatim.Coordinates, location *string) (*ForecastResult, error) { func (c *Client) ForecastCoords(coords *nominatim.Coordinates, location *string) (*ForecastResult, error) {
coordCacheName := fmt.Sprintf("%.4f_%.4f", coords.Latitude, coords.Longitude)
cacheResult, err := c.cache.Get(coordCacheName)
if err != nil {
return nil, err
}
if cacheResult != nil && !cache.IsExpired(cacheResult.Expires, nil) {
return cacheResult, nil
}
f, err := c.met.Forecast(coords.Latitude, coords.Longitude, nil) f, err := c.met.Forecast(coords.Latitude, coords.Longitude, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -173,13 +202,26 @@ func (c *Client) ForecastCoords(coords *nominatim.Coordinates, location *string)
} }
return &ForecastResult{ result := ForecastResult{
Expires: f.Expires,
LastModified: f.LastModified,
Coordinates: nominatim.Coordinates{ Coordinates: nominatim.Coordinates{
Longitude: coords.Longitude, Longitude: coords.Longitude,
Latitude: coords.Latitude, Latitude: coords.Latitude,
}, },
Forecast: forecasts, Forecast: forecasts,
}, nil }
err = c.cache.Add(*location, result)
if err != nil {
return nil, err
}
err = c.cache.Add(coordCacheName, result)
if err != nil {
return nil, err
}
return &result, nil
} }
func sortTimeSeries(a, b met.Timeseries) int { func sortTimeSeries(a, b met.Timeseries) int {