Skip to content

Commit

Permalink
Add support for fetching stores
Browse files Browse the repository at this point in the history
Add support for fetching stores using an alternate API.
  • Loading branch information
AlexGustafsson committed May 2, 2024
1 parent df8f729 commit 85c3786
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 4 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ Download the full assortment.
systembolaget assortment --sort-by "Name" --ascending
```

Fetch all stores.

```shell
systembolaget stores
```

An excerpt from the results is shown below. For samples, see the samples
directory.

Expand Down
2 changes: 1 addition & 1 deletion cmd/assortment.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"os/signal"
"time"

"github.com/alexgustafsson/systembolaget-api/v3/systembolaget"
"github.com/alexgustafsson/systembolaget-api/v4/systembolaget"
"github.com/urfave/cli/v2"
)

Expand Down
2 changes: 1 addition & 1 deletion cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"log/slog"
"os"

"github.com/alexgustafsson/systembolaget-api/v3/systembolaget"
"github.com/alexgustafsson/systembolaget-api/v4/systembolaget"
"github.com/urfave/cli/v2"
)

Expand Down
20 changes: 19 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"os"
"path/filepath"

"github.com/alexgustafsson/systembolaget-api/v3/systembolaget"
"github.com/alexgustafsson/systembolaget-api/v4/systembolaget"
"github.com/urfave/cli/v2"
)

Expand Down Expand Up @@ -192,6 +192,24 @@ func main() {
},
},
},
{
Name: "stores",
Usage: "Fetch stores",
Action: ActionStores,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "api-key",
Aliases: []string{"k"},
Usage: "API key to use. Defaults to automatically fetching one",
},
&cli.PathFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Path to output",
TakesFile: true,
},
},
},
}

if err := app.Run(os.Args); err != nil {
Expand Down
64 changes: 64 additions & 0 deletions cmd/stores.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"context"
"encoding/json"
"io"
"log/slog"
"os"
"os/signal"

"github.com/alexgustafsson/systembolaget-api/v4/systembolaget"
"github.com/urfave/cli/v2"
)

func ActionStores(ctx *cli.Context) error {
log := configureLogging(ctx)

apiKey, err := getAPIKey(ctx, log)
if err != nil {
return err
}

client := systembolaget.NewClient(apiKey)

var output io.Writer
if ctx.String("output") == "" {
output = os.Stdout
} else {
file, err := os.OpenFile(ctx.String("output"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
log.Error("Failed to open output file", slog.Any("error", err))
return err
}
defer file.Close()
output = file
}

runCtx, cancel := context.WithCancel(ctx.Context)

signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt)
caught := 0
go func() {
for range signals {
caught++
if caught == 1 {
slog.Info("Caught signal, exiting gracefully")
cancel()
} else {
slog.Info("Caught signal, exiting now")
os.Exit(1)
}
}
}()

stores, err := client.Stores(systembolaget.SetLogger(runCtx, log))
if err != nil {
return err
}

encoder := json.NewEncoder(output)
encoder.SetIndent("", " ")
return encoder.Encode(stores)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/alexgustafsson/systembolaget-api/v3
module github.com/alexgustafsson/systembolaget-api/v4

go 1.22

Expand Down
111 changes: 111 additions & 0 deletions samples/stores.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{
"siteId": "0102",
"alias": "Fältöversten",
"streetAddress": "Karlaplan 13",
"displayName": "Fältöversten",
"city": "STOCKHOLM",
"county": "Stockholms län",
"isAgent": false,
"isBlocked": false,
"blockedText": "",
"isOpen": true,
"isTastingStore": false,
"openingHours": [
{
"date": "2024-05-02T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
},
{
"date": "2024-05-03T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
},
{
"date": "2024-05-04T00:00:00",
"openFrom": "10:00:00",
"openTo": "15:00:00",
"reason": ""
},
{
"date": "2024-05-05T00:00:00",
"openFrom": "00:00:00",
"openTo": "00:00:00",
"reason": "-"
},
{
"date": "2024-05-06T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
},
{
"date": "2024-05-07T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
},
{
"date": "2024-05-08T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
},
{
"date": "2024-05-09T00:00:00",
"openFrom": "00:00:00",
"openTo": "00:00:00",
"reason": "Kristi Himmelfärdsdag"
},
{
"date": "2024-05-10T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
},
{
"date": "2024-05-11T00:00:00",
"openFrom": "10:00:00",
"openTo": "15:00:00",
"reason": ""
},
{
"date": "2024-05-12T00:00:00",
"openFrom": "00:00:00",
"openTo": "00:00:00",
"reason": "-"
},
{
"date": "2024-05-13T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
},
{
"date": "2024-05-14T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
},
{
"date": "2024-05-15T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
},
{
"date": "2024-05-16T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
},
{
"date": "2024-05-17T00:00:00",
"openFrom": "10:00:00",
"openTo": "19:00:00",
"reason": ""
}
]
}
109 changes: 109 additions & 0 deletions systembolaget/stores.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package systembolaget

import (
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
)

// Store represents a Systembolaget store.
type Store struct {
SiteID string `json:"siteId"`
Alias string `json:"alias"`
StreetAddress string `json:"streetAddress"`
DisplayName string `json:"displayName"`
City string `json:"city"`
County string `json:"county"`
// NOTE: always null, exclude for now
// PostalCode
IsAgent bool `json:"isAgent"`
IsBlocked bool `json:"isBlocked"`
BlockedText string `json:"blockedText"`
IsOpen bool `json:"isOpen"`
IsTastingStore bool `json:"isTastingStore"`
OpeningHours []StoreOpeningHours `json:"openingHours"`
}

type StoreOpeningHours struct {
Date string `json:"date"`
OpenFrom string `json:"openFrom"`
OpenTo string `json:"openTo"`
Reason string `json:"reason"`
}

// Stores fetches available stores.
func (c *Client) Stores(ctx context.Context) ([]Store, error) {
log := GetLogger(ctx)

query := url.Values{}
query.Set("includePredictions", "false")

u := &url.URL{
Scheme: "https",
Host: "api-extern.systembolaget.se",
Path: "/sb-api-ecommerce/v1/sitesearch/site",
RawQuery: query.Encode(),
}

log = log.With(slog.String("url", u.String()))

header := http.Header{}
header.Set("Origin", "https://www.systembolaget.se")
header.Set("Access-Control-Allow-Origin", "*")
header.Set("Pragma", "no-cache")
header.Set("Accept", "application/json")
header.Set("Accept-Encoding", "gzip")
header.Set("Cache-Control", "no-cache")
header.Set("Ocp-Apim-Subscription-Key", c.apiKey)

if c.userAgent != "" {
header.Set("User-Agent", c.userAgent)
}

req := (&http.Request{
Method: http.MethodGet,
URL: u,
Header: header,
}).Clone(ctx)

log.Debug("Performing request")
res, err := c.httpClient.Do(req)
if err != nil {
log.Error("Request failed")
return nil, err
}

if res.StatusCode != http.StatusOK {
log.Error("Got unexpected status code", slog.Int("statusCode", res.StatusCode), slog.String("status", res.Status))
return nil, fmt.Errorf("unexpected status code: %d - %s", res.StatusCode, res.Status)
}

var reader io.ReadCloser
switch res.Header.Get("Content-Encoding") {
case "gzip":
reader, err = gzip.NewReader(res.Body)
if err != nil {
log.Error("Failed to create gzip reader", slog.Any("error", err))
return nil, err
}
default:
reader = res.Body
}
defer reader.Close()

var result struct {
Stores []Store `json:"siteSearchResults"`
}
decoder := json.NewDecoder(reader)
if err := decoder.Decode(&result); err != nil {
log.Error("Failed to decode body", slog.Any("error", err))
return nil, err
}

return result.Stores, nil
}
28 changes: 28 additions & 0 deletions systembolaget/stores_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package systembolaget

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestStores(t *testing.T) {
if testing.Short() {
t.Skip()
}

client := NewClient(apiKey)

stores, err := client.Stores(context.TODO())
require.NoError(t, err)
fmt.Printf("%+v\n", stores)

for _, store := range stores {
if assert.NotNil(t, store) {
assert.NotEmpty(t, store.SiteID)
}
}
}

0 comments on commit 85c3786

Please sign in to comment.