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 {
|
type forecastH struct {
|
||||||
isJson bool
|
isJson bool
|
||||||
|
isDebug bool
|
||||||
isUTC bool
|
isUTC bool
|
||||||
isWeb bool
|
isWeb bool
|
||||||
f *yr.ForecastResult
|
f *yr.ForecastResult
|
||||||
|
@ -22,6 +23,8 @@ type forecastH struct {
|
||||||
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
11
pkg/cache/cache.go
vendored
|
@ -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()
|
||||||
|
}
|
||||||
|
|
25
pkg/cache/cache_test.go
vendored
25
pkg/cache/cache_test.go
vendored
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
48
yr/yr.go
48
yr/yr.go
|
@ -6,6 +6,7 @@ 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"
|
||||||
|
@ -14,6 +15,7 @@ import (
|
||||||
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 {
|
||||||
|
Expires time.Time `json:"expires"`
|
||||||
|
LastModified time.Time `json:"lastModified"`
|
||||||
Coordinates nominatim.Coordinates `json:"coordinates"`
|
Coordinates nominatim.Coordinates `json:"coordinates"`
|
||||||
Forecast []Forecast `json:"forecast"`
|
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 {
|
||||||
|
|
Loading…
Reference in a new issue