-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.go
More file actions
197 lines (165 loc) · 4.41 KB
/
Copy pathclient.go
File metadata and controls
197 lines (165 loc) · 4.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package aperiodic
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
const (
DefaultBaseURL = "https://aperiodic.io/api/v1"
DefaultTimeout = 60 * time.Second
// DemoAPIKey is the shared public demo key. Preview data is served against
// it, so users can query the whitelisted preview slice without signing up.
DemoAPIKey = "DEMO-KEY"
)
type APIError struct {
StatusCode int
Message string
Details []string
// Code is the machine-readable reason some responses carry, e.g.
// "raw_not_in_plan" or "range_too_long" on raw data requests.
Code string
// UpgradeURL accompanies a "raw_not_in_plan" refusal.
UpgradeURL string
}
func (e *APIError) Error() string {
return fmt.Sprintf("%d: %s", e.StatusCode, e.Message)
}
type AperiodicClient struct {
APIKey string
BaseURL string
HTTPClient *http.Client
}
func NewAperiodicClient(apiKey string) *AperiodicClient {
baseURL := os.Getenv("APERIODIC_API_URL")
if baseURL == "" {
baseURL = DefaultBaseURL
}
return &AperiodicClient{
APIKey: apiKey,
BaseURL: baseURL,
HTTPClient: &http.Client{
Timeout: DefaultTimeout,
},
}
}
func (c *AperiodicClient) getHeaders() http.Header {
header := make(http.Header)
header.Set("X-API-KEY", c.APIKey)
cfClientID := os.Getenv("CF_ACCESS_CLIENT_ID")
cfClientSecret := os.Getenv("CF_ACCESS_CLIENT_SECRET")
if cfClientID != "" && cfClientSecret != "" {
header.Set("CF-Access-Client-Id", cfClientID)
header.Set("CF-Access-Client-Secret", cfClientSecret)
}
return header
}
func (c *AperiodicClient) handleAPIError(resp *http.Response) error {
if resp.StatusCode == http.StatusOK {
return nil
}
apiErr := &APIError{
StatusCode: resp.StatusCode,
}
switch resp.StatusCode {
case http.StatusUnauthorized:
apiErr.Message = "Unauthorized"
case http.StatusForbidden:
// A 403 can carry a reason (e.g. raw_not_in_plan); keep it.
apiErr.Message = "Forbidden"
var errResp APIErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err == nil {
if errResp.Error != "" {
apiErr.Message = errResp.Error
}
apiErr.Details = errResp.Details
apiErr.Code = errResp.Code
apiErr.UpgradeURL = errResp.UpgradeURL
}
case http.StatusNotFound:
apiErr.Message = "Not Found"
case http.StatusTooManyRequests:
apiErr.Message = "Too Many Requests"
default:
var errResp APIErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err == nil {
apiErr.Message = errResp.Error
apiErr.Details = errResp.Details
apiErr.Code = errResp.Code
apiErr.UpgradeURL = errResp.UpgradeURL
} else {
body, _ := io.ReadAll(resp.Body)
apiErr.Message = string(body)
}
}
if apiErr.Message == "" {
apiErr.Message = "Unknown error"
}
return apiErr
}
func (c *AperiodicClient) GetSymbols(exchange string) ([]string, error) {
u, err := url.Parse(fmt.Sprintf("%s/metadata/symbols", c.BaseURL))
if err != nil {
return nil, err
}
q := u.Query()
q.Set("exchange", exchange)
u.RawQuery = q.Encode()
req := &http.Request{
Method: http.MethodGet,
URL: u,
Header: c.getHeaders(),
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := c.handleAPIError(resp); err != nil {
return nil, err
}
var symResp SymbolsResponse
if err := json.NewDecoder(resp.Body).Decode(&symResp); err != nil {
return nil, err
}
return symResp.Symbols, nil
}
func (c *AperiodicClient) FetchPresignedUrls(bucket string, timestamp TimestampType, interval Interval, exchange string, symbol string, startDate string, endDate string, preview bool) (*AggregateDataResponse, error) {
dataPath := fmt.Sprintf("%s/data/%s", c.BaseURL, bucket)
if preview {
dataPath = fmt.Sprintf("%s/data/preview/%s", c.BaseURL, bucket)
}
u, err := url.Parse(dataPath)
if err != nil {
return nil, err
}
q := u.Query()
q.Set("timestamp", string(timestamp))
q.Set("interval", string(interval))
q.Set("exchange", exchange)
q.Set("symbol", symbol)
q.Set("start_date", startDate)
q.Set("end_date", endDate)
u.RawQuery = q.Encode()
req := &http.Request{
Method: http.MethodGet,
URL: u,
Header: c.getHeaders(),
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := c.handleAPIError(resp); err != nil {
return nil, err
}
var dataResp AggregateDataResponse
if err := json.NewDecoder(resp.Body).Decode(&dataResp); err != nil {
return nil, err
}
return &dataResp, nil
}