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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,15 @@ quartr reports download 12345 --output annual-report.pdf
quartr slides download 12345 --url-field fileUrl
```

A download always writes a file unless you ask for stdout. `--output -` streams the document itself:

```bash
quartr transcripts download 432907 --output - > transcript.json
quartr transcripts download 432907 --output - | jq '.transcript.text'
```

Without `--output`, the file is named after the resource and id (`transcripts-432907.json`) in the current directory. The `Saved <path>` confirmation goes to **stderr**, so a plain `> file` redirect never captures it.

By default, the CLI does not include `x-api-key` when fetching a returned file URL. Add `--with-api-key` if your URL requires it:

```bash
Expand Down
74 changes: 74 additions & 0 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,80 @@ func TestCompaniesResolveRejectsNameSearch(t *testing.T) {
}
}

// downloadServer serves one document plus its metadata record.
func downloadServer(t *testing.T, body string) *httptest.Server {
t.Helper()
var srv *httptest.Server
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/documents/transcripts/abc":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":{"id":"abc","fileUrl":"` + srv.URL + `/file/transcript.json"}}`))
case "/file/transcript.json":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(body))
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}))
return srv
}

func TestDownloadToStdout(t *testing.T) {
const body = `{"transcript":"hello"}`
srv := downloadServer(t, body)
defer srv.Close()

dir := t.TempDir()
t.Chdir(dir)

var out, errOut bytes.Buffer
code := Run([]string{"--no-config", "--api-key", "secret", "--base-url", srv.URL,
"transcripts", "download", "abc", "--output", "-"}, &out, &errOut)
if code != 0 {
t.Fatalf("expected code 0, got %d; stderr=%s", code, errOut.String())
}
// stdout is the document, byte for byte — a redirect must capture this
// and nothing else.
if out.String() != body {
t.Fatalf("expected the document on stdout, got %q", out.String())
}
if errOut.Len() != 0 {
t.Fatalf("expected nothing on stderr, got %q", errOut.String())
}
// And no stray file is left behind next to the redirect target.
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
if len(entries) != 0 {
t.Fatalf("expected no files written, got %v", entries)
}
}

func TestDownloadStatusLineGoesToStderr(t *testing.T) {
srv := downloadServer(t, `{"ok":true}`)
defer srv.Close()

t.Chdir(t.TempDir())

var out, errOut bytes.Buffer
code := Run([]string{"--no-config", "--api-key", "secret", "--base-url", srv.URL,
"transcripts", "download", "abc"}, &out, &errOut)
if code != 0 {
t.Fatalf("expected code 0, got %d; stderr=%s", code, errOut.String())
}
if out.Len() != 0 {
t.Fatalf("expected clean stdout, got %q", out.String())
}
if !strings.Contains(errOut.String(), "Saved transcripts-abc.json") {
t.Fatalf("expected the saved path on stderr, got %q", errOut.String())
}
if _, err := os.Stat("transcripts-abc.json"); err != nil {
t.Fatalf("expected the default file to exist: %v", err)
}
}

func TestListAllFollowsPagination(t *testing.T) {
requests := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
34 changes: 25 additions & 9 deletions internal/cli/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func (a *app) downloadResource(r resource, args []string) error {
return fmt.Errorf("%s does not have a configured download URL field", r.name)
}
fs := newFlagSet(r.name+" download", a.errOut)
outPath := fs.String("output", "", "output file path; defaults to a name based on id and URL")
outPath := fs.String("output", "", "output file path, or - to stream to stdout; defaults to a name based on id and URL")
urlField := fs.String("url-field", r.downloadField, "metadata URL field to download")
withAPIKey := fs.Bool("with-api-key", false, "include x-api-key when fetching the file URL")
expand := fs.String("expand", "", "fields to expand on the metadata request")
Expand All @@ -330,27 +330,43 @@ func (a *app) downloadResource(r resource, args []string) error {
return err
}

apiKey := ""
if *withAPIKey {
apiKey = a.cfg.APIKey()
}

// `--output -` streams the document itself to stdout so it can be piped
// or redirected. Everything else this command prints goes to stderr, so
// `quartr transcripts download <id> --output - > f.json` writes the
// document and nothing else.
if *outPath == "-" {
_, err := a.client.Download(context.Background(), downloadURL, apiKey, a.out)
return err
}

dest := *outPath
if dest == "" {
dest = defaultFileName(r.name, id, downloadURL)
}
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil && filepath.Dir(dest) != "." {
return err
if dir := filepath.Dir(dest); dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
}
f, err := os.Create(dest)
if err != nil {
return err
}
defer f.Close()
defer func() { _ = f.Close() }()

apiKey := ""
if *withAPIKey {
apiKey = a.cfg.APIKey()
}
if _, err := a.client.Download(context.Background(), downloadURL, apiKey, f); err != nil {
return err
}
fmt.Fprintf(a.out, "Saved %s\n", dest)
if err := f.Close(); err != nil {
return fmt.Errorf("write %s: %w", dest, err)
}
// stderr, not stdout: a redirect is supposed to capture the document.
fmt.Fprintf(a.errOut, "Saved %s\n", dest)
return nil
}

Expand Down
10 changes: 9 additions & 1 deletion internal/cli/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ Common list flags:
strings.ReplaceAll(sortRecipe(r), "\n", "\n "))
}
}
if r.downloadField != "" {
fmt.Fprintf(a.out, `
Downloads:
download <id> writes ./%s-<id>.<ext> and reports the path on stderr
download <id> --output P writes P
download <id> --output - streams the document to stdout, nothing else
`, r.name)
}
fmt.Fprint(a.out, `
Examples:
`)
Expand All @@ -122,7 +130,7 @@ Examples:
case "events":
fmt.Fprint(a.out, " quartr events list --tickers AAPL --sort-by date --direction desc\n quartr events summary 128301 --length long --plain\n")
case "transcripts":
fmt.Fprint(a.out, " quartr transcripts list --tickers AAPL --expand event\n quartr transcripts download 432907 --output transcript.json\n")
fmt.Fprint(a.out, " quartr transcripts list --tickers AAPL --expand event\n quartr transcripts download 432907 --output transcript.json\n quartr transcripts download 432907 --output - | jq .\n")
case "live-transcripts":
fmt.Fprint(a.out, " quartr live transcripts list --states live,willBeLive\n quartr live transcripts stream 127537 --transcript-version 1.7\n")
default:
Expand Down
Loading