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
112 changes: 98 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,117 @@
# duners

A convenience library for executing queries and recovering results from Dune Analytics API.
A Rust client for the [Dune Analytics API](https://dune.com/docs/api/). Execute queries, wait for completion, and deserialize results into your own types.

## Installation and Usage
[![docs.rs](https://img.shields.io/docsrs/duners)](https://docs.rs/duners)
[![crates.io](https://img.shields.io/crates/v/duners)](https://crates.io/crates/duners)

```shell
## Installation

```bash
cargo add duners
```

You’ll need the **tokio** runtime (e.g. `tokio` with `rt-multi-thread` and `macros`).

## Quick start

1. **Get an API key** from [Dune → Settings → API](https://dune.com/settings/api).
2. **Set it** (or put it in a `.env` file as `DUNE_API_KEY=...`):

```bash
export DUNE_API_KEY="your-api-key"
```

3. **Run a query** using the `refresh` helper (execute → wait until done → return results):

```rust
use duners::{DuneClient, DuneRequestError};
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Row {
symbol: String,
max_price: f64,
}

#[tokio::main]
async fn main() -> Result<(), DuneRequestError> {
let client = DuneClient::from_env();
let result = client.refresh::<Row>(971694, None, None).await?;
println!("{:?}", result.get_rows());
Ok(())
}
```

The **query ID** (e.g. `971694`) is the number at the end of a Dune query URL: `https://dune.com/queries/971694`.

## Authentication

- **`DuneClient::new(api_key)`** — pass the API key directly.
- **`DuneClient::from_env()`** — reads `DUNE_API_KEY` from the environment. If a `.env` file exists in the current directory, it is loaded first.

## Parameterized queries

For queries that take parameters, pass a list of [`Parameter`](https://docs.rs/duners/latest/duners/parameters/struct.Parameter.html) as the second argument to `refresh` (or `execute_query`):

```rust
use duners::{DuneClient, Parameter};

let params = vec![
Parameter::text("WalletAddress", "0x1234..."),
Parameter::number("MinAmount", "100"),
Parameter::list("Token", "ETH"),
];
let result = client.refresh::<MyRow>(QUERY_ID, Some(params), None).await?;
```

Parameter names must match the names defined in the query on Dune.

## Deserializing result rows

Define a struct whose fields match the query’s columns and derive `Deserialize`. You can use your own types; the API often returns numbers and dates as **strings**, so use the helpers in [`parse_utils`](https://docs.rs/duners/latest/duners/parse_utils/index.html) when needed:

```rust
use chrono::{DateTime, Utc};
use duners::{client::DuneClient, dateutil::datetime_from_str};
use duners::parse_utils::{datetime_from_str, f64_from_str};
use serde::Deserialize;

// User must declare the expected query return fields and types!
#[derive(Deserialize, Debug, PartialEq)]
#[derive(Deserialize, Debug)]
struct ResultStruct {
text_field: String,
#[serde(deserialize_with = "f64_from_str")]
number_field: f64,
#[serde(deserialize_with = "datetime_from_str")]
date_field: DateTime<Utc>,
list_field: String,
}

#[tokio::main]
async fn main() -> Result<(), DuneRequestError> {
let dune = DuneClient::from_env();
let results = dune.refresh::<ResultStruct>(1215383, None, None).await?;
println!("{:?}", results.get_rows());
Ok(())
}
```

- **`f64_from_str`** — for numeric columns that come as strings.
- **`datetime_from_str`** — for date/timestamp columns that come as strings.

## Lower-level API

For more control (e.g. custom polling or cancellation):

- **`execute_query(query_id, params)`** — start execution; returns an `execution_id`.
- **`get_status(execution_id)`** — check status (`Complete`, `Executing`, `Pending`, `Cancelled`, `Failed`).
- **`get_results(execution_id)`** — fetch result rows (only valid when status is `Complete`).
- **`cancel_execution(execution_id)`** — cancel a running execution.

See the [API docs](https://docs.rs/duners) for details and types.

## Error handling

All fallible methods return `Result<_, DuneRequestError>`. Use `?` to propagate. `DuneRequestError` implements `std::error::Error` and `Display`; variants are:

- **`DuneRequestError::Dune(msg)`** — API returned an error (e.g. invalid API key, query not found).
- **`DuneRequestError::Request(msg)`** — network/HTTP error (e.g. connection failed, timeout).

## Documentation

Full API reference: **[docs.rs/duners](https://docs.rs/duners/latest/duners/)**

## License

MIT OR Apache-2.0
58 changes: 32 additions & 26 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,42 @@ use tokio::time::{sleep, Duration};

const BASE_URL: &str = "https://api.dune.com/api/v1";

/// DuneClient provides an interface for interacting with Dune Analytics API.
/// Official Documentation here: [https://dune.com/docs/api/](https://dune.com/docs/api/).
/// Client for the [Dune Analytics API](https://dune.com/docs/api/).
///
/// Elementary Routes (i.e. those provided by Dune)
/// - POST
/// - execute_query
/// - cancel_execution
/// - GET
/// - get_status
/// - get_results
/// Create a client with [`DuneClient::new`] (pass the API key directly) or [`DuneClient::from_env`]
/// (reads `DUNE_API_KEY` from the environment, including from a `.env` file if present).
///
/// Furthermore, this interface also implements a convenience method `refresh` which acts as follows:
/// 1. Execute query
/// 2. While execution status is not in a terminal state, sleep and check again
/// 3. Get and return execution results.
/// ## High-level usage
///
/// Use **[`refresh`](DuneClient::refresh)** to execute a query, wait until it finishes, and get
/// the result rows in one call. This is the easiest way to run a query.
///
/// ## Low-level usage
///
/// For more control (e.g. polling yourself or cancelling), use:
/// - **[`execute_query`](DuneClient::execute_query)** — Start a query, get an `execution_id`.
/// - **[`get_status`](DuneClient::get_status)** — Check whether the execution is still running.
/// - **[`get_results`](DuneClient::get_results)** — Fetch the result rows (only valid when complete).
/// - **[`cancel_execution`](DuneClient::cancel_execution)** — Cancel a running execution.
pub struct DuneClient {
/// An essential value for request authentication.
/// API key used for request authentication.
api_key: String,
}

impl DuneClient {
/// Constructor
/// Creates a client with the given API key.
///
/// Get your API key from [Dune → Settings → API](https://dune.com/settings/api).
pub fn new(api_key: &str) -> DuneClient {
DuneClient {
api_key: api_key.to_string(),
}
}

/// Creates a client using the `DUNE_API_KEY` environment variable.
///
/// Loads `.env` from the current directory if present (via the `dotenv` crate).
/// Panics if `DUNE_API_KEY` is not set.
pub fn from_env() -> DuneClient {
dotenv().ok();
DuneClient {
Expand Down Expand Up @@ -157,17 +166,14 @@ impl DuneClient {
/// (i.e. Too Many Requests) especially when executing multiple queries in parallel.
///
/// # Examples
/// ```
/// use duners::{
/// client::DuneClient,
/// parse_utils::{datetime_from_str, f64_from_str},
/// error::DuneRequestError
/// };
///
/// ```no_run
/// use duners::{DuneClient, DuneRequestError};
/// use duners::parse_utils::{datetime_from_str, f64_from_str};
/// use serde::Deserialize;
/// use chrono::{DateTime, Utc};
///
/// // User must declare the expected query return types and fields.
/// #[derive(Deserialize, Debug, PartialEq)]
/// #[derive(Deserialize, Debug)]
/// struct ResultStruct {
/// text_field: String,
/// #[serde(deserialize_with = "f64_from_str")]
Expand All @@ -179,9 +185,9 @@ impl DuneClient {
///
/// #[tokio::main]
/// async fn main() -> Result<(), DuneRequestError> {
/// let dune = DuneClient::from_env();
/// let results = dune.refresh::<ResultStruct>(1215383, None, None).await?;
/// println!("{:?}", results.get_rows());
/// let client = DuneClient::from_env();
/// let result = client.refresh::<ResultStruct>(1215383, None, None).await?;
/// println!("{:?}", result.get_rows());
/// Ok(())
/// }
/// ```
Expand Down
30 changes: 23 additions & 7 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
use serde::Deserialize;
use std::fmt;

/// Encapsulates any "unexpected" data
/// returned from Dune upon bad request.
/// Error payload returned by the Dune API when a request fails (e.g. invalid API key, query not found).
#[derive(Deserialize, Debug)]
pub struct DuneError {
/// Human-readable error message from Dune.
pub error: String,
}

/// All errors that can occur when calling the Dune API or parsing responses.
///
/// Use `?` in async functions that return `Result<_, DuneRequestError>` to propagate errors.
/// Implements [`std::error::Error`] and [`Display`](fmt::Display) for logging and error reporting.
#[derive(Debug, PartialEq)]
pub enum DuneRequestError {
/// Includes known errors:
/// "invalid API Key"
/// "Query not found"
/// "The requested execution ID (ID: wonky job ID) is invalid."
/// Error returned by the Dune API. Common messages include:
/// - `"invalid API Key"`
/// - `"Query not found"`
/// - `"The requested execution ID (ID: ) is invalid."`
Dune(String),
/// Errors bubbled up from reqwest::Error
/// Network or HTTP errors from the underlying request (e.g. connection failed, timeout).
Request(String),
}

impl fmt::Display for DuneRequestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DuneRequestError::Dune(msg) => write!(f, "Dune API error: {}", msg),
DuneRequestError::Request(msg) => write!(f, "request error: {}", msg),
}
}
}

impl std::error::Error for DuneRequestError {}

impl From<DuneError> for DuneRequestError {
fn from(value: DuneError) -> Self {
DuneRequestError::Dune(value.error)
Expand Down
55 changes: 50 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,55 @@
/// DuneClient structure and all API route implementations.
//! # duners
//!
//! A Rust client for the [Dune Analytics API](https://dune.com/docs/api/). Execute queries, poll for
//! completion, and deserialize results into your own types.
//!
//! ## Quick start
//!
//! 1. **Get an API key** from [Dune](https://dune.com/settings/api) and set it:
//! ```bash
//! export DUNE_API_KEY="your-api-key"
//! ```
//!
//! 2. **Add the dependency** and run a query:
//!
//! ```rust,no_run
//! use duners::{DuneClient, DuneRequestError};
//! use serde::Deserialize;
//!
//! #[derive(Deserialize, Debug)]
//! struct Row {
//! symbol: String,
//! max_price: f64,
//! }
//!
//! #[tokio::main]
//! async fn main() -> Result<(), DuneRequestError> {
//! let client = DuneClient::from_env();
//! let result = client.refresh::<Row>(971694, None, None).await?;
//! println!("{:?}", result.get_rows());
//! Ok(())
//! }
//! ```
//!
//! ## What’s in this crate
//!
//! - **[`DuneClient`](client::DuneClient)** — Main entry point. Create with [`DuneClient::new`](client::DuneClient::new) or [`DuneClient::from_env`](client::DuneClient::from_env).
//! - **[`refresh`](client::DuneClient::refresh)** — Run a query and wait for results (execute → poll status → return rows).
//! - **Lower-level API** — [`execute_query`](client::DuneClient::execute_query), [`get_status`](client::DuneClient::get_status), [`get_results`](client::DuneClient::get_results), [`cancel_execution`](client::DuneClient::cancel_execution) for full control.
//! - **[`Parameter`](parameters::Parameter)** — Query parameters (text, number, date, list) for parameterized queries.
//! - **[`parse_utils`](parse_utils)** — Helpers for deserializing Dune’s JSON (e.g. dates and numbers that come as strings): [`datetime_from_str`](parse_utils::datetime_from_str), [`f64_from_str`](parse_utils::f64_from_str).
//! - **[`DuneRequestError`](error::DuneRequestError)** — All request and parsing errors.
//!
//! See the [README](https://github.com/bh2smith/duners) for more examples and details.

pub mod client;
/// DuneRequestError (encapsulating all errors that could arise within network requests and result parsing)
pub mod error;
/// Content related to Query Parameters.
pub mod parameters;
/// Utility Methods (primarily for date parsing)
pub mod parse_utils;
/// Data models representing response types for all client methods.
pub mod response;

// Re-export commonly used types for convenience and clearer docs.
pub use client::DuneClient;
pub use error::DuneRequestError;
pub use parameters::Parameter;
pub use response::{ExecutionStatus, GetResultResponse};
Loading