Skip to content

Commit 5eccfa2

Browse files
authored
Dataset search command APP-7392 (#11)
* Dataset search command * use new sdk version * Add usage command * Add readme file
1 parent f704916 commit 5eccfa2

10 files changed

Lines changed: 604 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,60 @@
11
# Dune CLI
2+
3+
A command-line interface for interacting with the Dune Analytics API.
4+
5+
## Authentication
6+
7+
```bash
8+
# Save your API key to ~/.config/dune/config.yaml
9+
dune auth --api-key <key>
10+
11+
# Or set via environment variable
12+
export DUNE_API_KEY=<key>
13+
```
14+
15+
The `--api-key` flag is available on all commands to override the stored key.
16+
17+
## Commands
18+
19+
### `dune query`
20+
21+
Manage and execute Dune queries.
22+
23+
| Command | Description |
24+
|---------|-------------|
25+
| `query create --name <name> --sql <sql> [--description] [--private]` | Create a new saved query |
26+
| `query get <query-id>` | Get a saved query's details and SQL |
27+
| `query update <query-id> [--name] [--sql] [--description] [--private] [--tags]` | Update an existing query |
28+
| `query archive <query-id>` | Archive a saved query |
29+
| `query run <query-id> [--param key=value] [--performance medium\|large] [--limit] [--no-wait]` | Execute a saved query and display results |
30+
| `query run-sql --sql <sql> [--param key=value] [--performance medium\|large] [--limit] [--no-wait]` | Execute raw SQL directly |
31+
32+
### `dune execution`
33+
34+
Manage query executions.
35+
36+
| Command | Description |
37+
|---------|-------------|
38+
| `execution results <execution-id> [--limit] [--offset]` | Fetch results of a query execution |
39+
40+
### `dune dataset`
41+
42+
Search the Dune dataset catalog.
43+
44+
| Command | Description |
45+
|---------|-------------|
46+
| `dataset search [--query] [--categories] [--blockchains] [--schemas] [--dataset-types] [--owner-scope] [--include-private] [--include-schema] [--include-metadata] [--limit] [--offset]` | Search for datasets |
47+
48+
Categories: `canonical`, `decoded`, `spell`, `community`
49+
50+
### `dune usage`
51+
52+
Show credit and resource usage for your account.
53+
54+
```bash
55+
dune usage [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]
56+
```
57+
58+
## Output Format
59+
60+
Most commands support `-o, --output <format>` with `text` (default) or `json`.

‎cli/root.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99
"github.com/charmbracelet/fang"
1010
"github.com/duneanalytics/cli/authconfig"
1111
"github.com/duneanalytics/cli/cmd/auth"
12+
"github.com/duneanalytics/cli/cmd/dataset"
1213
"github.com/duneanalytics/cli/cmd/execution"
1314
"github.com/duneanalytics/cli/cmd/query"
15+
"github.com/duneanalytics/cli/cmd/usage"
1416
"github.com/duneanalytics/cli/cmdutil"
1517
"github.com/duneanalytics/duneapi-client-go/config"
1618
"github.com/duneanalytics/duneapi-client-go/dune"
@@ -63,8 +65,10 @@ var rootCmd = &cobra.Command{
6365
func init() {
6466
rootCmd.PersistentFlags().StringVar(&apiKeyFlag, "api-key", "", "Dune API key (overrides DUNE_API_KEY env var)")
6567
rootCmd.AddCommand(auth.NewAuthCmd())
68+
rootCmd.AddCommand(dataset.NewDatasetCmd())
6669
rootCmd.AddCommand(query.NewQueryCmd())
6770
rootCmd.AddCommand(execution.NewExecutionCmd())
71+
rootCmd.AddCommand(usage.NewUsageCmd())
6872
}
6973

7074
// Execute runs the root command via Fang.

‎cmd/dataset/dataset.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package dataset
2+
3+
import "github.com/spf13/cobra"
4+
5+
// NewDatasetCmd returns the `dataset` parent command.
6+
func NewDatasetCmd() *cobra.Command {
7+
cmd := &cobra.Command{
8+
Use: "dataset",
9+
Short: "Manage Dune datasets",
10+
}
11+
cmd.AddCommand(newSearchCmd())
12+
return cmd
13+
}

‎cmd/dataset/search.go‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package dataset
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/duneanalytics/cli/cmdutil"
8+
"github.com/duneanalytics/cli/output"
9+
"github.com/duneanalytics/duneapi-client-go/models"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
func newSearchCmd() *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "search",
16+
Short: "Search for datasets across the Dune catalog",
17+
RunE: runSearch,
18+
}
19+
20+
cmd.Flags().String("query", "", "search query text")
21+
cmd.Flags().StringArray("categories", nil, "filter by category (canonical, decoded, spell, community)")
22+
cmd.Flags().StringArray("blockchains", nil, "filter by blockchain")
23+
cmd.Flags().StringArray("dataset-types", nil, "filter by dataset type")
24+
cmd.Flags().StringArray("schemas", nil, "filter by schema")
25+
cmd.Flags().String("owner-scope", "", "ownership filter (all, me, team)")
26+
cmd.Flags().Bool("include-private", false, "include private datasets")
27+
cmd.Flags().Bool("include-schema", false, "include column schema in results")
28+
cmd.Flags().Bool("include-metadata", false, "include metadata in results")
29+
cmd.Flags().Int32("limit", 20, "maximum number of results")
30+
cmd.Flags().Int32("offset", 0, "pagination offset")
31+
output.AddFormatFlag(cmd, "text")
32+
33+
return cmd
34+
}
35+
36+
func runSearch(cmd *cobra.Command, _ []string) error {
37+
client := cmdutil.ClientFromCmd(cmd)
38+
39+
req := models.SearchDatasetsRequest{}
40+
41+
if cmd.Flags().Changed("query") {
42+
v, _ := cmd.Flags().GetString("query")
43+
req.Query = &v
44+
}
45+
if cmd.Flags().Changed("categories") {
46+
v, _ := cmd.Flags().GetStringArray("categories")
47+
req.Categories = v
48+
}
49+
if cmd.Flags().Changed("blockchains") {
50+
v, _ := cmd.Flags().GetStringArray("blockchains")
51+
req.Blockchains = v
52+
}
53+
if cmd.Flags().Changed("dataset-types") {
54+
v, _ := cmd.Flags().GetStringArray("dataset-types")
55+
req.DatasetTypes = v
56+
}
57+
if cmd.Flags().Changed("schemas") {
58+
v, _ := cmd.Flags().GetStringArray("schemas")
59+
req.Schemas = v
60+
}
61+
if cmd.Flags().Changed("owner-scope") {
62+
v, _ := cmd.Flags().GetString("owner-scope")
63+
req.OwnerScope = &v
64+
}
65+
if cmd.Flags().Changed("include-private") {
66+
v, _ := cmd.Flags().GetBool("include-private")
67+
req.IncludePrivate = &v
68+
}
69+
if cmd.Flags().Changed("include-schema") {
70+
v, _ := cmd.Flags().GetBool("include-schema")
71+
req.IncludeSchema = &v
72+
}
73+
if cmd.Flags().Changed("include-metadata") {
74+
v, _ := cmd.Flags().GetBool("include-metadata")
75+
req.IncludeMetadata = &v
76+
}
77+
if cmd.Flags().Changed("limit") {
78+
v, _ := cmd.Flags().GetInt32("limit")
79+
req.Limit = &v
80+
}
81+
if cmd.Flags().Changed("offset") {
82+
v, _ := cmd.Flags().GetInt32("offset")
83+
req.Offset = &v
84+
}
85+
86+
resp, err := client.SearchDatasets(req)
87+
if err != nil {
88+
return err
89+
}
90+
91+
w := cmd.OutOrStdout()
92+
switch output.FormatFromCmd(cmd) {
93+
case output.FormatJSON:
94+
return output.PrintJSON(w, resp)
95+
default:
96+
columns := []string{"FULL_NAME", "CATEGORY", "DATASET_TYPE", "BLOCKCHAINS"}
97+
rows := make([][]string, len(resp.Results))
98+
for i, r := range resp.Results {
99+
dt := ""
100+
if r.DatasetType != nil {
101+
dt = *r.DatasetType
102+
}
103+
rows[i] = []string{
104+
r.FullName,
105+
r.Category,
106+
dt,
107+
strings.Join(r.Blockchains, ", "),
108+
}
109+
}
110+
output.PrintTable(w, columns, rows)
111+
fmt.Fprintf(w, "\n%d of %d results\n", len(resp.Results), resp.Total)
112+
return nil
113+
}
114+
}

‎cmd/dataset/search_test.go‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package dataset_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"testing"
9+
10+
"github.com/duneanalytics/cli/cmd/dataset"
11+
"github.com/duneanalytics/cli/cmdutil"
12+
"github.com/duneanalytics/duneapi-client-go/dune"
13+
"github.com/duneanalytics/duneapi-client-go/models"
14+
"github.com/spf13/cobra"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
type mockClient struct {
20+
dune.DuneClient
21+
searchDatasetsFn func(models.SearchDatasetsRequest) (*models.SearchDatasetsResponse, error)
22+
}
23+
24+
func (m *mockClient) SearchDatasets(req models.SearchDatasetsRequest) (*models.SearchDatasetsResponse, error) {
25+
return m.searchDatasetsFn(req)
26+
}
27+
28+
func newTestRoot(mock dune.DuneClient) (*cobra.Command, *bytes.Buffer) {
29+
root := &cobra.Command{
30+
Use: "dune",
31+
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
32+
cmdutil.SetClient(cmd, mock)
33+
},
34+
}
35+
root.SetContext(context.Background())
36+
root.AddCommand(dataset.NewDatasetCmd())
37+
38+
var buf bytes.Buffer
39+
root.SetOut(&buf)
40+
41+
return root, &buf
42+
}
43+
44+
func TestSearchSuccess(t *testing.T) {
45+
dt := "spell"
46+
var gotReq models.SearchDatasetsRequest
47+
48+
mock := &mockClient{
49+
searchDatasetsFn: func(req models.SearchDatasetsRequest) (*models.SearchDatasetsResponse, error) {
50+
gotReq = req
51+
return &models.SearchDatasetsResponse{
52+
Total: 1,
53+
Results: []models.SearchDatasetResult{
54+
{
55+
FullName: "dex.trades",
56+
Category: "spell",
57+
DatasetType: &dt,
58+
Blockchains: []string{"ethereum", "arbitrum"},
59+
},
60+
},
61+
Pagination: models.SearchDatasetsPagination{
62+
Limit: 5,
63+
Offset: 0,
64+
HasMore: false,
65+
},
66+
}, nil
67+
},
68+
}
69+
70+
root, buf := newTestRoot(mock)
71+
root.SetArgs([]string{
72+
"dataset", "search",
73+
"--query", "dex trades",
74+
"--categories", "spell",
75+
"--blockchains", "ethereum",
76+
"--limit", "5",
77+
})
78+
79+
require.NoError(t, root.Execute())
80+
81+
// Verify flags mapped to request
82+
assert.Equal(t, "dex trades", *gotReq.Query)
83+
assert.Equal(t, []string{"spell"}, gotReq.Categories)
84+
assert.Equal(t, []string{"ethereum"}, gotReq.Blockchains)
85+
assert.Equal(t, int32(5), *gotReq.Limit)
86+
87+
// Verify table output
88+
out := buf.String()
89+
assert.Contains(t, out, "dex.trades")
90+
assert.Contains(t, out, "spell")
91+
assert.Contains(t, out, "ethereum, arbitrum")
92+
assert.Contains(t, out, "1 of 1 results")
93+
}
94+
95+
func TestSearchJSON(t *testing.T) {
96+
mock := &mockClient{
97+
searchDatasetsFn: func(req models.SearchDatasetsRequest) (*models.SearchDatasetsResponse, error) {
98+
return &models.SearchDatasetsResponse{
99+
Total: 1,
100+
Results: []models.SearchDatasetResult{
101+
{
102+
FullName: "ethereum.transactions",
103+
Category: "canonical",
104+
},
105+
},
106+
Pagination: models.SearchDatasetsPagination{
107+
Limit: 20,
108+
Offset: 0,
109+
HasMore: false,
110+
},
111+
}, nil
112+
},
113+
}
114+
115+
root, buf := newTestRoot(mock)
116+
root.SetArgs([]string{"dataset", "search", "--query", "ethereum", "-o", "json"})
117+
118+
require.NoError(t, root.Execute())
119+
120+
var resp models.SearchDatasetsResponse
121+
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
122+
assert.Equal(t, int32(1), resp.Total)
123+
assert.Equal(t, "ethereum.transactions", resp.Results[0].FullName)
124+
}
125+
126+
func TestSearchError(t *testing.T) {
127+
mock := &mockClient{
128+
searchDatasetsFn: func(req models.SearchDatasetsRequest) (*models.SearchDatasetsResponse, error) {
129+
return nil, fmt.Errorf("API error: unauthorized")
130+
},
131+
}
132+
133+
root, _ := newTestRoot(mock)
134+
root.SetArgs([]string{"dataset", "search", "--query", "test"})
135+
136+
err := root.Execute()
137+
require.Error(t, err)
138+
assert.Contains(t, err.Error(), "API error: unauthorized")
139+
}

0 commit comments

Comments
 (0)