Updates Containerize
This commit is contained in:
23
.github/workflows/ci.yaml
vendored
Normal file
23
.github/workflows/ci.yaml
vendored
Normal 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
71
Dockerfile
Normal 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
37
Makefile
Normal 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 .
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Go Tastytrade Open API Wrapper
|
# Go Tastytrade Open API Wrapper
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Go API wrapper for the Tastytrade Open API
|
Go API wrapper for the Tastytrade Open API
|
||||||
|
|
||||||
|
|||||||
7
api.go
7
api.go
@@ -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
6
go.mod
@@ -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
2
go.sum
Normal 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=
|
||||||
58
sessions.go
58
sessions.go
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user