Updates Containerize

This commit is contained in:
sjc
2025-05-07 23:50:46 -04:00
parent 67aa1221aa
commit 0b022d7623
9 changed files with 237 additions and 9 deletions

23
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Continuous Integration
on: [push]
jobs:
ci:
name: CI
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run linter
run: make lint
- name: Run unit tests
run: make unit-test
- name: Get unit test coverage
run: make unit-test-coverage
- name: Build Linux binary
run: make PLATFORM=linux/amd64
- name: Build Windows binary
run: make PLATFORM=windows/amd64

71
Dockerfile Normal file
View File

@@ -0,0 +1,71 @@
# Containerized-go dockerfile
FROM --platform=${BUILDPLATFORM} golang:1.24.3-alpine AS base
WORKDIR /src
ENV CGO_ENABLED=1
RUN apk update
RUN apk add gcc g++
RUN mkdir /out
FROM base AS buildbase
COPY go.* .
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
FROM buildbase AS build
ARG TARGETOS
ARG TARGETARCH
RUN --mount=target=. \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/example .
FROM base AS unit-test
RUN --mount=target=. \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go test -v -coverprofile=/out/cover.out ./...
FROM golangci/golangci-lint:v2.1.6-alpine AS lint-base
FROM base AS lint
RUN --mount=target=. \
--mount=from=lint-base,src=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/.cache/golangci-lint \
golangci-lint run --timeout 10m0s ./...
FROM base AS mod-init-base
WORKDIR /out
COPY . .
ENV MODNAME tastytrade
RUN go mod init "${MODNAME}" && go mod tidy
FROM base AS mod-tidy
WORKDIR /out
COPY . .
RUN go mod tidy
FROM scratch AS mod-init
COPY --from=mod-init-base /out/go.mod /go.mod
COPY --from=mod-init-base /out/go.sum /go.sum
FROM scratch AS tidy
COPY --from=mod-tidy /out/go.mod /go.mod
COPY --from=mod-tidy /out/go.sum /go.sum
COPY --from=mod-tidy /out/go.mod /go.mod
FROM scratch AS unit-test-coverage
COPY --from=unit-test /out/cover.out /cover.out
FROM scratch AS bin-unix
COPY --from=build /out/example /
FROM bin-unix AS bin-linux
FROM bin-unix AS bin-darwin
FROM scratch AS bin-windows
COPY --from=build /out/example /example.exe
FROM bin-${TARGETOS} AS bin

37
Makefile Normal file
View File

@@ -0,0 +1,37 @@
all: bin/example
test: lint unit-test
PLATFORM=local
UTIL_TAG=1.24.3-alpine
export DOCKER_BUILDKIT=1
.PHONY: bin/example
bin/example:
@docker build . --target bin \
--output bin/ \
--platform ${PLATFORM}
.PHONY: unit-test
unit-test:
@docker build . --target unit-test
.PHONY: unit-test-coverage
unit-test-coverage:
@docker build . --target unit-test-coverage \
--output coverage/
cat coverage/cover.out
.PHONY: lint
lint:
@docker build . --target lint
.PHONY: tidy
tidy:
export DOCKER_BUILDKIT=1
@docker build . --target tidy --output .
.PHONY: init
init:
export DOCKER_BUILDKIT=1
@docker build . --target mod-init --output .

View File

@@ -1,6 +1,6 @@
# Go Tastytrade Open API Wrapper # Go Tastytrade Open API Wrapper
![Build](https://sancus.carpanet.net/sjc/tastytrade/actions/workflows/build.yaml/badge.svg) ![Build](https://sancus.carpanet.net/sjc/tastytrade/actions/workflows/ci.yaml/badge.svg)
Go API wrapper for the Tastytrade Open API Go API wrapper for the Tastytrade Open API

7
api.go
View File

@@ -14,7 +14,9 @@ const (
// TastytradeAPI represents the Tastytrade API client // TastytradeAPI represents the Tastytrade API client
type TastytradeAPI struct { type TastytradeAPI struct {
httpClient *http.Client httpClient *http.Client
user string
authToken string authToken string
authExpire time.Time
remToken string remToken string
host string host string
} }
@@ -31,6 +33,11 @@ func NewTastytradeAPI(hosts ...string) *TastytradeAPI {
} }
} }
// get logged in User
func (api *TastytradeAPI) User() string {
return api.user
}
// fetchData sends a GET request to the specified URL with authorization // fetchData sends a GET request to the specified URL with authorization
func (api *TastytradeAPI) fetchData(url string) (map[string]interface{}, error) { func (api *TastytradeAPI) fetchData(url string) (map[string]interface{}, error) {
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)

6
go.mod
View File

@@ -1,3 +1,5 @@
module sancus.carpanet.net/sjc/tastytrade module tastytrade
go 1.23 go 1.24.3
require sancus.carpanet.net/sjc/tastytrade v0.0.0-20250507051921-46dbff053071

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
sancus.carpanet.net/sjc/tastytrade v0.0.0-20250507051921-46dbff053071 h1:tkqr+I0vRU0f1VHW/rwZQ6Iv9viIgo1ThpLXxo/CoBI=
sancus.carpanet.net/sjc/tastytrade v0.0.0-20250507051921-46dbff053071/go.mod h1:w85F/rGX4Lt1UrvVtRE+muoc4wI4A0GSMqXIj2mN8FI=

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"time"
) )
type User struct { type User struct {
@@ -15,8 +16,9 @@ type User struct {
} }
type AuthData struct { type AuthData struct {
User User `json:"user"` User User `json:"user"`
SessionToken string `json:"session-token"` SessionToken string `json:"session-token"`
SessionExpire time.Time `json:"session-expiration"`
RememberToken string `json:"remember-token"` RememberToken string `json:"remember-token"`
} }
@@ -59,9 +61,10 @@ func (api *TastytradeAPI) Authenticate2(username, password string, remember bool
return err return err
} }
api.authToken = authResponse.Data.SessionToken api.authToken = authResponse.Data.SessionToken
if remember { api.authExpire = authResponse.Data.SessionExpire
api.remToken = authResponse.Data.RememberToken if (remember) {
api.remToken = authResponse.Data.RememberToken
} }
return nil return nil
} }
@@ -69,3 +72,48 @@ func (api *TastytradeAPI) Authenticate2(username, password string, remember bool
func (api *TastytradeAPI) Authenticate(username, password string) error { func (api *TastytradeAPI) Authenticate(username, password string) error {
return api.Authenticate2(username, password, false) return api.Authenticate2(username, password, false)
} }
// Redeem a remember token to get a session
func (api *TastytradeAPI) AuthRemember(remember bool) error {
var remStr string
if remember {
remStr = "true"
} else {
remStr = "false"
}
authURL := fmt.Sprintf("%s/sessions", api.host)
authData := map[string]string{
"login": api.user,
"remember-token": api.remToken,
"remember-me": remStr,
}
authBody, err := json.Marshal(authData)
if err != nil {
return err
}
resp, err := api.httpClient.Post(authURL, "application/json", bytes.NewReader(authBody))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("authentication failed: status code %d", resp.StatusCode)
}
authResponse := AuthResponse{}
if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil {
return err
}
api.authToken = authResponse.Data.SessionToken
if (remember) {
api.remToken = authResponse.Data.RememberToken
} else {
api.remToken = ""
}
return nil
}

View File

@@ -3,6 +3,7 @@ package tastytrade
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"time"
"testing" "testing"
) )
@@ -30,7 +31,7 @@ func TestAuthenticate(t *testing.T) {
func TestAuthenticate2(t *testing.T) { func TestAuthenticate2(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"user": {"email": "test@example.com", "username": "testuser", "external-id": "123", "is-confirmed": true}, "session-token": "testtoken", "remember-token": "remember"}}`)) rw.Write([]byte(`{"context": "test", "data": {"user": {"email": "test@example.com", "username": "testuser", "external-id": "123", "is-confirmed": true}, "session-token": "testtoken", "remember-token": "remember", "session-expiration": "2024-09-12T20:25:32.440Z"}}`))
})) }))
defer server.Close() defer server.Close()
@@ -48,4 +49,41 @@ func TestAuthenticate2(t *testing.T) {
if (api.remToken != "remember") { if (api.remToken != "remember") {
t.Errorf("Expected remember token remember, got remember token %s", api.remToken) t.Errorf("Expected remember token remember, got remember token %s", api.remToken)
} }
et, _ := time.Parse(time.RFC3339, "2024-09-12T20:25:32.440Z")
if (api.authExpire != et) {
t.Errorf("Expected expire %s, got %s", et, api.authExpire)
}
}
func TestAuthRemember(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"user": {"email": "test@example.com", "username": "testuser", "external-id": "123", "is-confirmed": true}, "session-token": "testtoken", "remember-token": "remember-new"}}`))
}))
var err error
defer server.Close()
api := NewTastytradeAPI(server.URL)
err = api.Authenticate2("testuser", "testpassword", true)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
api.remToken = "XXBADVALUEXX"
err = api.AuthRemember(true)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if (api.remToken != "remember-new") {
t.Errorf("Expected remember token remember, got remember token %s", api.remToken)
}
err = api.AuthRemember(false)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if (len(api.remToken) > 0) {
t.Errorf("Expected empty remember token, got remember token %s", api.remToken)
}
} }