Compare commits
10 commits
dbcaca54c7
...
ffe81b005d
Author | SHA1 | Date | |
---|---|---|---|
|
ffe81b005d | ||
|
9449cba68d | ||
|
0d9cea8b95 | ||
|
858b336bcf | ||
|
14f46806cd | ||
|
358ac49eea | ||
|
203a623ed4 | ||
|
23ce4a6e39 | ||
|
b09c928d45 | ||
|
2077eb5a25 |
12 changed files with 83 additions and 43 deletions
10
.build.yml
10
.build.yml
|
@ -22,9 +22,7 @@ tasks:
|
||||||
complete-build
|
complete-build
|
||||||
fi
|
fi
|
||||||
|
|
||||||
version=$(echo "$tag" | tr -d 'v')
|
make GOOS="darwin" ARCH="amd64" TAG=$tag build-release
|
||||||
|
make GOOS="darwin" ARCH="aarch64" TAG=$tag build-release
|
||||||
make GOOS="darwin" ARCH="amd64" build-release
|
make GOOS="linux" ARCH="amd64" TAG=$tag build-release
|
||||||
make GOOS="dawrin" ARCH="aarch64" build-release
|
make GOOS="windows" ARCH="amd64" TAG=$tag build-release
|
||||||
make GOOS="linux" ARCH="amd64" build-release
|
|
||||||
make GOOS="windows" ARCH="amd64" build-release
|
|
||||||
|
|
6
Makefile
6
Makefile
|
@ -8,11 +8,11 @@ endif
|
||||||
bindir = $(prefix)/bin
|
bindir = $(prefix)/bin
|
||||||
builddir = ./build
|
builddir = ./build
|
||||||
|
|
||||||
VERSION = 0.0.5
|
VERSION = 0.0.6
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build:
|
build:
|
||||||
go build -o $(builddir)
|
go build -o $(builddir)/
|
||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: build
|
install: build
|
||||||
|
@ -30,7 +30,7 @@ build-release: build
|
||||||
cp $(builddir)/yr* yr-$(VERSION)/
|
cp $(builddir)/yr* yr-$(VERSION)/
|
||||||
|
|
||||||
tar czf yr-$(VERSION)-$(GOOS)-$(ARCH).tar.gz yr-$(VERSION)
|
tar czf yr-$(VERSION)-$(GOOS)-$(ARCH).tar.gz yr-$(VERSION)
|
||||||
hut git artifact upload --rev $(tag) yr-$(VERSION)-$(GOOS)-$(ARCH).tar.gz
|
hut git artifact upload --rev $(TAG) yr-$(VERSION)-$(GOOS)-$(ARCH).tar.gz
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
|
|
|
@ -21,7 +21,7 @@ var (
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(forecastCmd)
|
rootCmd.AddCommand(forecastCmd)
|
||||||
rootCmd.Flags().IntVarP(&interval, "interval", "i", 0, "Number of hours to output")
|
forecastCmd.Flags().IntVarP(&interval, "interval", "i", 0, "Number of hours to output")
|
||||||
}
|
}
|
||||||
|
|
||||||
func forecast(cmd *cobra.Command, args []string) {
|
func forecast(cmd *cobra.Command, args []string) {
|
||||||
|
@ -39,12 +39,13 @@ func forecast(cmd *cobra.Command, args []string) {
|
||||||
outputJson(f.Forecast)
|
outputJson(f.Forecast)
|
||||||
}
|
}
|
||||||
|
|
||||||
t := table.New()
|
t := table.New("time", "temp.", "rain", "wind")
|
||||||
|
|
||||||
for _, item := range f.Forecast {
|
for _, item := range f.Forecast {
|
||||||
if !isUTC {
|
if !isUTC {
|
||||||
item.Time = item.Time.Local()
|
item.Time = item.Time.Local()
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Row(
|
t.Row(
|
||||||
item.Time.Format("Mon, 2 Jan 15:04"),
|
item.Time.Format("Mon, 2 Jan 15:04"),
|
||||||
fmt.Sprintf("%.1f °C", item.Temperature),
|
fmt.Sprintf("%.1f °C", item.Temperature),
|
||||||
|
|
|
@ -7,8 +7,7 @@ import (
|
||||||
"github.com/charmbracelet/lipgloss/table"
|
"github.com/charmbracelet/lipgloss/table"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Make more generic
|
func New(headers ...string) *table.Table {
|
||||||
func New() *table.Table {
|
|
||||||
re := lipgloss.NewRenderer(os.Stdout)
|
re := lipgloss.NewRenderer(os.Stdout)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -16,23 +15,41 @@ func New() *table.Table {
|
||||||
lightGray = lipgloss.Color("#dedede")
|
lightGray = lipgloss.Color("#dedede")
|
||||||
)
|
)
|
||||||
var (
|
var (
|
||||||
HeaderStyle = re.NewStyle().Foreground(white).Bold(true).Align(lipgloss.Center)
|
HeaderStyle = re.
|
||||||
CellStyle = re.NewStyle().Padding(0, 2)
|
NewStyle().
|
||||||
|
Foreground(white).
|
||||||
|
Bold(true).
|
||||||
|
Align(lipgloss.Center)
|
||||||
|
|
||||||
|
CellStyle = re.NewStyle().
|
||||||
|
Padding(0, 2).
|
||||||
|
AlignHorizontal(lipgloss.Right)
|
||||||
|
|
||||||
EvenRowStyle = CellStyle.Foreground(lightGray)
|
EvenRowStyle = CellStyle.Foreground(lightGray)
|
||||||
)
|
)
|
||||||
|
|
||||||
t := table.New().
|
t := table.New().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
StyleFunc(func(row, col int) lipgloss.Style {
|
StyleFunc(func(row, col int) lipgloss.Style {
|
||||||
switch {
|
if row == 0 {
|
||||||
case row == 0:
|
|
||||||
return HeaderStyle
|
return HeaderStyle
|
||||||
case row%2 == 0:
|
|
||||||
return EvenRowStyle
|
|
||||||
default:
|
|
||||||
return CellStyle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var style lipgloss.Style
|
||||||
|
switch {
|
||||||
|
case row%2 == 0:
|
||||||
|
style = EvenRowStyle
|
||||||
|
default:
|
||||||
|
style = CellStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
// right align numeric values for better readability
|
||||||
|
if col > 0 && row > 1 {
|
||||||
|
style.AlignHorizontal(lipgloss.Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
return style
|
||||||
}).
|
}).
|
||||||
Headers("time", "temp.", "rain", "wind")
|
Headers(headers...)
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "yr",
|
Use: "yr",
|
||||||
Short: "yr is CLI tool for weather forecasts",
|
Short: "yr is CLI tool for weather forecasts",
|
||||||
Version: "v0.0.5",
|
Version: "v0.0.6",
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
|
|
|
@ -30,7 +30,7 @@ func today(cmd *cobra.Command, args []string) {
|
||||||
outputJson(f.Forecast)
|
outputJson(f.Forecast)
|
||||||
}
|
}
|
||||||
|
|
||||||
t := table.New()
|
t := table.New("time", "temp.", "rain", "wind")
|
||||||
|
|
||||||
today := time.Now()
|
today := time.Now()
|
||||||
for _, item := range f.Forecast {
|
for _, item := range f.Forecast {
|
||||||
|
|
|
@ -36,7 +36,7 @@ func tomorrow(cmd *cobra.Command, args []string) {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
t := table.New()
|
t := table.New("time", "temp.", "rain", "wind")
|
||||||
|
|
||||||
tomorrows := []yr.Forecast{}
|
tomorrows := []yr.Forecast{}
|
||||||
|
|
||||||
|
|
28
pkg/cache/cache.go
vendored
28
pkg/cache/cache.go
vendored
|
@ -23,22 +23,32 @@ func New[T any](path string) *Cache[T] {
|
||||||
return &Cache[T]{path}
|
return &Cache[T]{path}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrorUnableToMarshalJSON = errors.New("unable to marshal as JSON")
|
||||||
|
ErrorUnableToEnsureCacheDir = errors.New("unable to ensure cache-dir is present")
|
||||||
|
ErrorUnableToWriteCache = errors.New("unable to write data to cache file")
|
||||||
|
ErrorUnableToOpenCacheFile = errors.New("unable to open cache")
|
||||||
|
ErrorUnableToReadCacheFile = errors.New("unable to read cache")
|
||||||
|
ErrorUnableToUnmarshalCacheFile = errors.New("unable to unmarshal JSON from cache-file")
|
||||||
|
ErrorUnableToReadCachePath = errors.New("unable to read from cache path")
|
||||||
|
)
|
||||||
|
|
||||||
// Add data as JSON-file into cache-dir, returns nil if success
|
// Add data as JSON-file into cache-dir, returns nil if success
|
||||||
func (c *Cache[T]) Add(name string, data any) error {
|
func (c *Cache[T]) Add(name string, data any) error {
|
||||||
j, err := json.MarshalIndent(data, "", " ")
|
j, err := json.MarshalIndent(data, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to convert data as JSON.")
|
return ErrorUnableToMarshalJSON
|
||||||
}
|
}
|
||||||
err = os.MkdirAll(c.Path, 0646)
|
err = os.MkdirAll(c.Path, 0646)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to ensure cache-dir is present.")
|
return ErrorUnableToEnsureCacheDir
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath := filepath.Join(c.Path, filename(name))
|
filePath := filepath.Join(c.Path, filename(name))
|
||||||
d := []byte(j)
|
d := []byte(j)
|
||||||
err = os.WriteFile(filePath, d, 0646)
|
err = os.WriteFile(filePath, d, 0646)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed write data to cache-file.")
|
return ErrorUnableToWriteCache
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -53,19 +63,19 @@ func (c *Cache[T]) Get(name string) (*T, error) {
|
||||||
|
|
||||||
rawFile, err := os.Open(filePath)
|
rawFile, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to read cache-file.")
|
return nil, ErrorUnableToOpenCacheFile
|
||||||
}
|
}
|
||||||
|
|
||||||
var result T
|
var result T
|
||||||
|
|
||||||
bytes, err := io.ReadAll(rawFile)
|
bytes, err := io.ReadAll(rawFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to read data from cache-file.")
|
return nil, ErrorUnableToReadCacheFile
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(bytes, &result)
|
err = json.Unmarshal(bytes, &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to parse JSON from cache-file.")
|
return nil, ErrorUnableToUnmarshalCacheFile
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
|
@ -80,7 +90,7 @@ func (c *Cache[T]) List() ([]Saved[T], error) {
|
||||||
var result []Saved[T]
|
var result []Saved[T]
|
||||||
filenames, err := list(c.Path)
|
filenames, err := list(c.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to read from cache path")
|
return nil, ErrorUnableToReadCachePath
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filenames) == 0 {
|
if len(filenames) == 0 {
|
||||||
|
@ -102,7 +112,7 @@ func (c *Cache[T]) List() ([]Saved[T], error) {
|
||||||
func list(path string) ([]string, error) {
|
func list(path string) ([]string, error) {
|
||||||
dir, err := os.ReadDir(path)
|
dir, err := os.ReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to read from path")
|
return nil, ErrorUnableToReadCachePath
|
||||||
}
|
}
|
||||||
var filenames []string
|
var filenames []string
|
||||||
|
|
||||||
|
@ -145,5 +155,5 @@ func IsExpired(expire time.Time, now *time.Time) bool {
|
||||||
now = &tmp
|
now = &tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
return now.Unix() >= expire.Unix()
|
return now.After(expire)
|
||||||
}
|
}
|
||||||
|
|
2
pkg/cache/cache_test.go
vendored
2
pkg/cache/cache_test.go
vendored
|
@ -55,7 +55,7 @@ func TestIsExpired(t *testing.T) {
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
|
|
||||||
result = IsExpired(expirationTime, &now)
|
result = IsExpired(expirationTime, &now)
|
||||||
assert.True(result)
|
assert.False(result)
|
||||||
|
|
||||||
now, err = time.Parse(layout, "2024-10-04 22:23:00")
|
now, err = time.Parse(layout, "2024-10-04 22:23:00")
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package met
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -13,10 +14,18 @@ type Met struct {
|
||||||
siteName string
|
siteName string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrorMissingSiteName = errors.New("siteName must be defined")
|
||||||
|
ErrorMissingHeaderExpires = errors.New("missing header 'Expires'")
|
||||||
|
ErrorMissingHeaderLastModified = errors.New("missing header 'Last-Modified'")
|
||||||
|
ErrorParseExpires = errors.New("unable to parse 'Expires'")
|
||||||
|
ErrorParseLastModified = errors.New("unable to parse 'Last-Modified'")
|
||||||
|
)
|
||||||
|
|
||||||
// Returns valid Met client or error if empty siteName is provided
|
// Returns valid Met client or error if empty siteName is provided
|
||||||
func New(siteName string) (*Met, error) {
|
func New(siteName string) (*Met, error) {
|
||||||
if siteName == "" {
|
if siteName == "" {
|
||||||
return nil, fmt.Errorf("`siteName` must be defined.")
|
return nil, ErrorMissingSiteName
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Met{
|
return &Met{
|
||||||
|
@ -60,19 +69,19 @@ func (m *Met) Forecast(lat, lon float64, alt *int) (*LocationForecastResult, err
|
||||||
|
|
||||||
expiresRaw, ok := resp.Header["Expires"]
|
expiresRaw, ok := resp.Header["Expires"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("Unable to get 'Expires' header")
|
return nil, ErrorMissingHeaderExpires
|
||||||
}
|
}
|
||||||
expires, err := time.Parse(time.RFC1123, expiresRaw[0])
|
expires, err := time.Parse(time.RFC1123, expiresRaw[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to parse `expiresRaw`: %w", err)
|
return nil, errors.Join(ErrorParseExpires, err)
|
||||||
}
|
}
|
||||||
lastModifiedRaw, ok := resp.Header["Last-Modified"]
|
lastModifiedRaw, ok := resp.Header["Last-Modified"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("Unable to get 'Last-Modified' header")
|
return nil, ErrorMissingHeaderLastModified
|
||||||
}
|
}
|
||||||
lastModified, err := time.Parse(time.RFC1123, lastModifiedRaw[0])
|
lastModified, err := time.Parse(time.RFC1123, lastModifiedRaw[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to parse `Last-Modified`: %w", err)
|
return nil, errors.Join(ErrorParseLastModified, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
forecast := LocationForecast{}
|
forecast := LocationForecast{}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package nominatim
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -14,9 +15,13 @@ type Nominatim struct {
|
||||||
siteName string
|
siteName string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrorMissingSiteName = errors.New("siteName must be defined")
|
||||||
|
)
|
||||||
|
|
||||||
func New(siteName string) (*Nominatim, error) {
|
func New(siteName string) (*Nominatim, error) {
|
||||||
if siteName == "" {
|
if siteName == "" {
|
||||||
return nil, fmt.Errorf("`siteName` must be defined.")
|
return nil, ErrorMissingSiteName
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Nominatim{
|
return &Nominatim{
|
||||||
|
@ -94,7 +99,7 @@ func (n *Nominatim) Reverse(lat, lon float64) (*ReverseResult, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if allFieldsNil(result) {
|
if allFieldsNil(result) {
|
||||||
return nil, fmt.Errorf("No result")
|
return nil, fmt.Errorf("no result")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
|
|
2
yr/yr.go
2
yr/yr.go
|
@ -165,7 +165,7 @@ func (c *Client) ForecastCoords(coords *nominatim.Coordinates, location *string)
|
||||||
}
|
}
|
||||||
|
|
||||||
if location == nil {
|
if location == nil {
|
||||||
return nil, fmt.Errorf("Location name is not set")
|
return nil, fmt.Errorf("location name is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := f.Properties.Timeseries
|
ts := f.Properties.Timeseries
|
||||||
|
|
Loading…
Add table
Reference in a new issue