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 {
isJson bool
isUTC bool
isWeb bool
f *yr.ForecastResult
isJson bool
isDebug bool
isUTC bool
isWeb bool
f *yr.ForecastResult
}
func forecastHelper(cmd *cobra.Command, args []string) *forecastH {
isJson, err := cmd.Flags().GetBool(flags.JSON)
cobra.CheckErr(err)
isDebug, err := cmd.Flags().GetBool(flags.DEBUG)
cobra.CheckErr(err)
isUTC, err := cmd.Flags().GetBool(flags.UTC)
cobra.CheckErr(err)
isWeb, err := cmd.Flags().GetBool(flags.WEB)
@ -46,8 +49,15 @@ func forecastHelper(cmd *cobra.Command, args []string) *forecastH {
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{
isJson,
isDebug,
isUTC,
isWeb,
f,

11
pkg/cache/cache.go vendored
View file

@ -7,6 +7,7 @@ import (
"io"
"os"
"path/filepath"
"time"
)
type Cache[T any] struct {
@ -67,3 +68,13 @@ func (c *Cache[T]) Get(name string) (*T, error) {
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 (
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
@ -25,3 +26,27 @@ func TestCache(t *testing.T) {
assert.Equal("test1", result.Prop1)
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"
"io"
"net/http"
"time"
)
type Met struct {
@ -53,11 +54,32 @@ func (m *Met) Forecast(lat, lon float64, alt *int) (*LocationForecastResult, 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)
if err != nil {
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"
type cacheInfo struct {
type LocationForecastResult struct {
Expires time.Time `json:"expires"`
LastModified time.Time `json:"lastModified"`
LocationForecast
}
type LocationForecastResult struct {
type LocationForecast struct {
Type string `json:"type"`
Geometry struct {

View file

@ -6,14 +6,16 @@ import (
"slices"
"time"
"git.sr.ht/~timharek/yr/pkg/cache"
"git.sr.ht/~timharek/yr/pkg/met"
"git.sr.ht/~timharek/yr/pkg/nominatim"
"git.sr.ht/~timharek/yr/yr/direction"
)
type Client struct {
met met.Met
nom nominatim.Nominatim
met met.Met
nom nominatim.Nominatim
cache cache.Cache[ForecastResult]
}
func New() (*Client, error) {
@ -26,8 +28,9 @@ func New() (*Client, error) {
if err != nil {
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 {
@ -74,11 +77,21 @@ type Forecast struct {
}
type ForecastResult struct {
Coordinates nominatim.Coordinates `json:"coordinates"`
Forecast []Forecast `json:"forecast"`
Expires time.Time `json:"expires"`
LastModified time.Time `json:"lastModified"`
Coordinates nominatim.Coordinates `json:"coordinates"`
Forecast []Forecast `json:"forecast"`
}
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)
if err != nil {
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) {
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)
if err != nil {
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) {
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)
if err != nil {
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{
Longitude: coords.Longitude,
Latitude: coords.Latitude,
},
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 {