Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,36 @@ chmod +x aperiodic

## Authentication

Set your API key as an environment variable:
Sign in once per machine:

```bash
export APERIODIC_API_KEY=your_api_key
aperiodic login
```

Get your API key at [aperiodic.io](https://aperiodic.io).
It opens your browser, or, over SSH or on a machine without a display, prints a link to open on any device. Check the code there matches the one in your terminal and allow it. A free account works: it covers every metric for 2025 on BTC, ETH, SOL and 10 RWA perps.

For [preview data](#preview-data) (`--preview`), no API key is required — the CLI uses the shared public demo key automatically.
The machine gets a key of its own, named after it and saved in `~/.config/aperiodic/credentials` (readable only by you; `%APPDATA%\aperiodic` on Windows, or `$APERIODIC_CONFIG_DIR`). The Python client reads the same file. See and revoke each machine's key at [aperiodic.io/home/api](https://aperiodic.io/home/api).

```bash
aperiodic whoami # which key is in use, and what the plan covers
aperiodic logout # revoke this machine's key and remove it
```

Running `aperiodic login` again keeps a saved key that still works. `aperiodic login --force` replaces it with a new one and revokes the old one, so keys don't pile up on your account. If a saved key was revoked, every command says so and asks you to sign in again.

For scheduled jobs, CI and containers, set a key instead. Make a named key for the job at [aperiodic.io/home/api](https://aperiodic.io/home/api):

```bash
export APERIODIC_API_KEY=apk_…
```

Every command picks its key in this order:

1. `APERIODIC_API_KEY`, when it is set;
2. the shared public demo key, for a `--preview` request;
3. the key `aperiodic login` saved.

So [preview data](#preview-data) (`--preview`) needs no API key, and a saved key never takes over a preview request: even a revoked one can't break the free sample. The Python client uses the same order.

[Raw data](#raw-data) needs a key on the Prime + Raw plan; `aperiodic raw coverage` and `aperiodic raw --preview` work without one.

Expand Down
85 changes: 66 additions & 19 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,49 @@ import (
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)

type CLI struct {
Stdout io.Writer
Stderr io.Writer
Env func(string) string
// OpenURL opens a link in the browser; login falls back to printing it.
OpenURL func(string) error

// client, sleepFunc and nowFunc let the package's tests drive login
// without waiting; nil means the real ones.
client *http.Client
sleepFunc func(time.Duration)
nowFunc func() time.Time
}

func NewCLI() *CLI {
return &CLI{
Stdout: os.Stdout,
Stderr: os.Stderr,
Env: os.Getenv,
Stdout: os.Stdout,
Stderr: os.Stderr,
Env: os.Getenv,
OpenURL: openBrowser,
}
}

func (c *CLI) sleep(d time.Duration) {
if c.sleepFunc != nil {
c.sleepFunc(d)
return
}
time.Sleep(d)
}

func (c *CLI) now() time.Time {
if c.nowFunc != nil {
return c.nowFunc()
}
return time.Now()
}

func (c *CLI) Run(args []string) int {
if len(args) == 0 {
c.printUsage()
Expand All @@ -33,6 +59,15 @@ func (c *CLI) Run(args []string) int {
return 0
}

switch cmd {
case "login":
return c.runLogin(args[1:])
case "logout":
return c.runLogout(args[1:])
case "whoami":
return c.runWhoami(args[1:])
}

if cmd == "raw" {
return c.runRaw(args[1:])
}
Expand All @@ -57,47 +92,52 @@ func (c *CLI) Run(args []string) int {
return 2
}

apiKey, ok := c.resolveAPIKey(*previewFlag)
apiKey, source, ok := c.resolveAPIKey(*previewFlag)
if !ok {
return 1
}

client := NewAperiodicClient(apiKey)

if cmd == "symbols" {
return c.handleSymbols(client, *exchangeFlag)
return c.handleSymbols(client, *exchangeFlag, source)
}

if *outputDirFlag == "" {
fmt.Fprintln(c.Stderr, "Error: --output-dir is mandatory")
return 1
}

return c.handleData(client, cmd, *timestampFlag, *intervalFlag, *exchangeFlag, *symbolFlag, *startDateFlag, *endDateFlag, *maxConcurrentFlag, *outputDirFlag, *previewFlag)
return c.handleData(client, source, cmd, *timestampFlag, *intervalFlag, *exchangeFlag, *symbolFlag, *startDateFlag, *endDateFlag, *maxConcurrentFlag, *outputDirFlag, *previewFlag)
}

func isHelpArg(arg string) bool {
return arg == "help" || arg == "-h" || arg == "--help"
}

// resolveAPIKey returns APERIODIC_API_KEY or, for a preview request without
// one, the shared demo key. It reports the missing key itself.
func (c *CLI) resolveAPIKey(preview bool) (string, bool) {
// resolveAPIKey returns APERIODIC_API_KEY, else, for a preview request, the
// shared demo key, else the key `aperiodic login` saved, with where it came
// from. A saved key never answers a preview request, so a revoked one cannot
// break the free sample. It reports the missing key itself.
func (c *CLI) resolveAPIKey(preview bool) (apiKey, source string, ok bool) {
if apiKey := c.Env("APERIODIC_API_KEY"); apiKey != "" {
return apiKey, true
return apiKey, "APERIODIC_API_KEY", true
}
if preview {
// Preview data is served against the shared demo key, so no key is required.
return DemoAPIKey, true
return DemoAPIKey, "the shared demo key", true
}
if apiKey, source, ok := c.findAPIKey(); ok {
return apiKey, source, true
}
fmt.Fprintln(c.Stderr, "Error: APERIODIC_API_KEY environment variable not set (pass --preview to use the shared demo key)")
return "", false
fmt.Fprintln(c.Stderr, "Error: no API key. Run 'aperiodic login', set APERIODIC_API_KEY, or pass --preview for the free sample.")
return "", "", false
}

func (c *CLI) printUsage() {
fmt.Fprintln(c.Stdout, "Aperiodic CLI Client")
fmt.Fprintln(c.Stdout)
fmt.Fprintln(c.Stdout, "Usage:")
fmt.Fprintln(c.Stdout, " aperiodic login [--no-browser] [--name NAME] [--force]")
fmt.Fprintln(c.Stdout, " aperiodic <metric> [flags]")
fmt.Fprintln(c.Stdout, " aperiodic symbols [flags]")
fmt.Fprintln(c.Stdout, " aperiodic raw <dataset> [flags]")
Expand Down Expand Up @@ -126,13 +166,16 @@ func (c *CLI) printUsage() {
fmt.Fprintln(c.Stdout, " derivative_price Derivative price")
fmt.Fprintln(c.Stdout)
fmt.Fprintln(c.Stdout, "Commands:")
fmt.Fprintln(c.Stdout, " login Sign in and save a key for this machine (works over SSH too)")
fmt.Fprintln(c.Stdout, " logout Revoke this machine's key and remove it")
fmt.Fprintln(c.Stdout, " whoami Show which key is in use and what the plan covers")
fmt.Fprintln(c.Stdout, " symbols List available symbols for an exchange")
fmt.Fprintln(c.Stdout, " raw Download raw trades, quotes and derivative ticks (Prime + Raw plan; see 'aperiodic raw help')")
fmt.Fprintln(c.Stdout, " stream Stream live rows over WebSocket as JSON lines (see 'aperiodic stream help')")
fmt.Fprintln(c.Stdout, " help Show this help")
fmt.Fprintln(c.Stdout)
fmt.Fprintln(c.Stdout, "Environment:")
fmt.Fprintln(c.Stdout, " APERIODIC_API_KEY Aperiodic API key (required, except with --preview)")
fmt.Fprintln(c.Stdout, " APERIODIC_API_KEY Aperiodic API key; used before the key 'aperiodic login' saved")
fmt.Fprintln(c.Stdout)
fmt.Fprintln(c.Stdout, "Flags:")
fmt.Fprintln(c.Stdout, " -end-date string")
Expand All @@ -155,10 +198,12 @@ func (c *CLI) printUsage() {
fmt.Fprintln(c.Stdout, " Timestamp source (exchange, true) (default \"exchange\")")
}

func (c *CLI) handleSymbols(client *AperiodicClient, exchange string) int {
func (c *CLI) handleSymbols(client *AperiodicClient, exchange, keySource string) int {
symbols, err := client.GetSymbols(exchange)
if err != nil {
fmt.Fprintf(c.Stderr, "Error fetching symbols: %v\n", err)
if !c.reportRejectedKey(statusOf(err), keySource) {
fmt.Fprintf(c.Stderr, "Error fetching symbols: %v\n", err)
}
return 1
}

Expand All @@ -169,7 +214,7 @@ func (c *CLI) handleSymbols(client *AperiodicClient, exchange string) int {
return 0
}

func (c *CLI) handleData(client *AperiodicClient, metric, timestamp, interval, exchange, symbol, startDate, endDate string, maxConcurrent int, outputDir string, preview bool) int {
func (c *CLI) handleData(client *AperiodicClient, keySource, metric, timestamp, interval, exchange, symbol, startDate, endDate string, maxConcurrent int, outputDir string, preview bool) int {
if symbol == "" {
fmt.Fprintln(c.Stderr, "Error: --symbol is required")
return 1
Expand All @@ -181,7 +226,9 @@ func (c *CLI) handleData(client *AperiodicClient, metric, timestamp, interval, e

resp, err := client.FetchPresignedUrls(metric, TimestampType(timestamp), Interval(interval), exchange, symbol, startDate, endDate, preview)
if err != nil {
fmt.Fprintf(c.Stderr, "Error fetching file URLs: %v\n", err)
if !c.reportRejectedKey(statusOf(err), keySource) {
fmt.Fprintf(c.Stderr, "Error fetching file URLs: %v\n", err)
}
return 1
}

Expand Down
Loading
Loading