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:
parent
cb00a14019
commit
437f7f5f9a
6 changed files with 126 additions and 15 deletions
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
type forecastH struct {
|
||||
isJson bool
|
||||
isDebug bool
|
||||
isUTC bool
|
||||
isWeb bool
|
||||
f *yr.ForecastResult
|
||||
|
@ -22,6 +23,8 @@ type forecastH struct {
|
|||
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
11
pkg/cache/cache.go
vendored
|
@ -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()
|
||||
}
|
||||
|
|
25
pkg/cache/cache_test.go
vendored
25
pkg/cache/cache_test.go
vendored
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
48
yr/yr.go
48
yr/yr.go
|
@ -6,6 +6,7 @@ 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"
|
||||
|
@ -14,6 +15,7 @@ import (
|
|||
type Client struct {
|
||||
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 {
|
||||
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 {
|
||||
|
|
Loading…
Reference in a new issue