Skip to main content

databento/
lib.rs

1// Use README as crate-level documentation
2#![doc = include_str!("../README.md")]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![deny(missing_docs)]
5#![deny(rustdoc::broken_intra_doc_links)]
6#![deny(clippy::missing_errors_doc)]
7
8#[cfg(feature = "historical")]
9mod deserialize;
10pub mod error;
11#[cfg(feature = "historical")]
12pub mod historical;
13#[cfg(feature = "live")]
14pub mod live;
15#[cfg(feature = "historical")]
16pub mod reference;
17
18pub use error::{Error, Result};
19#[cfg(feature = "historical")]
20#[doc(inline)]
21pub use historical::Client as HistoricalClient;
22#[cfg(feature = "live")]
23#[doc(inline)]
24pub use live::Client as LiveClient;
25#[cfg(feature = "historical")]
26#[doc(inline)]
27pub use reference::Client as ReferenceClient;
28// Re-export to keep versions synchronized
29pub use dbn;
30
31use std::{
32    fmt::{self, Display, Write},
33    sync::LazyLock,
34};
35
36#[cfg(feature = "historical")]
37use serde::{Deserialize, Deserializer};
38use tracing::error;
39
40/// A set of symbols for a particular [`SType`](dbn::enums::SType).
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum Symbols {
43    /// Sentinel value for all symbols in a dataset.
44    All,
45    /// A set of symbols identified by their instrument IDs.
46    Ids(Vec<u32>),
47    /// A set of symbols.
48    Symbols(Vec<String>),
49}
50
51const ALL_SYMBOLS: &str = "ALL_SYMBOLS";
52const API_KEY_LENGTH: usize = 32;
53
54impl Symbols {
55    /// Returns the string representation for sending to the API.
56    pub fn to_api_string(&self) -> String {
57        match self {
58            Symbols::All => ALL_SYMBOLS.to_owned(),
59            Symbols::Ids(ids) => ids.iter().fold(String::new(), |mut acc, s| {
60                if acc.is_empty() {
61                    s.to_string()
62                } else {
63                    write!(acc, ",{s}").unwrap();
64                    acc
65                }
66            }),
67            Symbols::Symbols(symbols) => symbols.join(","),
68        }
69    }
70
71    #[cfg(feature = "live")]
72    /// Splits the symbol into chunks to stay within the message length requirements of
73    /// the live gateway.
74    pub fn to_chunked_api_string(&self) -> Vec<String> {
75        const CHUNK_SIZE: usize = 500;
76        match self {
77            Symbols::All => vec![ALL_SYMBOLS.to_owned()],
78            Symbols::Ids(ids) => ids
79                .chunks(CHUNK_SIZE)
80                .map(|chunk| {
81                    chunk.iter().fold(String::new(), |mut acc, s| {
82                        if acc.is_empty() {
83                            s.to_string()
84                        } else {
85                            write!(acc, ",{s}").unwrap();
86                            acc
87                        }
88                    })
89                })
90                .collect(),
91            Symbols::Symbols(symbols) => symbols
92                .chunks(CHUNK_SIZE)
93                .map(|chunk| chunk.join(","))
94                .collect(),
95        }
96    }
97}
98
99impl Display for Symbols {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        match self {
102            Symbols::All => f.write_str(ALL_SYMBOLS),
103            Symbols::Ids(ids) => {
104                for (i, id) in ids.iter().enumerate() {
105                    if i == 0 {
106                        write!(f, "{id}")?;
107                    } else {
108                        write!(f, ", {id}")?;
109                    }
110                }
111                Ok(())
112            }
113            Symbols::Symbols(symbols) => {
114                for (i, sym) in symbols.iter().enumerate() {
115                    if i == 0 {
116                        write!(f, "{sym}")?;
117                    } else {
118                        write!(f, ", {sym}")?;
119                    }
120                }
121                Ok(())
122            }
123        }
124    }
125}
126
127impl From<&str> for Symbols {
128    fn from(value: &str) -> Self {
129        Symbols::Symbols(vec![value.to_owned()])
130    }
131}
132
133impl From<u32> for Symbols {
134    fn from(value: u32) -> Self {
135        Symbols::Ids(vec![value])
136    }
137}
138
139impl From<Vec<u32>> for Symbols {
140    fn from(value: Vec<u32>) -> Self {
141        Symbols::Ids(value)
142    }
143}
144
145impl From<String> for Symbols {
146    fn from(value: String) -> Self {
147        Symbols::Symbols(vec![value])
148    }
149}
150
151impl From<Vec<String>> for Symbols {
152    fn from(value: Vec<String>) -> Self {
153        Symbols::Symbols(value)
154    }
155}
156
157impl<const N: usize> From<[&str; N]> for Symbols {
158    fn from(value: [&str; N]) -> Self {
159        Symbols::Symbols(value.iter().map(ToString::to_string).collect())
160    }
161}
162
163impl From<&[&str]> for Symbols {
164    fn from(value: &[&str]) -> Self {
165        Symbols::Symbols(value.iter().map(ToString::to_string).collect())
166    }
167}
168
169impl From<Vec<&str>> for Symbols {
170    fn from(value: Vec<&str>) -> Self {
171        Symbols::Symbols(value.into_iter().map(ToOwned::to_owned).collect())
172    }
173}
174
175pub(crate) fn key_from_env() -> crate::Result<String> {
176    std::env::var("DATABENTO_API_KEY").map_err(|e| {
177        Error::bad_arg(
178            "key",
179            match e {
180                std::env::VarError::NotPresent => "tried to read API key from environment variable DATABENTO_API_KEY but it is not set",
181                std::env::VarError::NotUnicode(_) => {
182                    "environment variable DATABENTO_API_KEY contains invalid unicode"
183                }
184            },
185        )
186    })
187}
188
189#[cfg(feature = "historical")]
190impl<'de> Deserialize<'de> for Symbols {
191    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
192    where
193        D: Deserializer<'de>,
194    {
195        #[derive(Deserialize)]
196        #[serde(untagged)]
197        enum Helper {
198            Id(u32),
199            Ids(Vec<u32>),
200            Symbol(String),
201            Symbols(Vec<String>),
202        }
203        let ir = Helper::deserialize(deserializer)?;
204        Ok(match ir {
205            Helper::Id(id) => Symbols::Ids(vec![id]),
206            Helper::Ids(ids) => Symbols::Ids(ids),
207            Helper::Symbol(symbol) if symbol == ALL_SYMBOLS => Symbols::All,
208            Helper::Symbol(symbol) => Symbols::Symbols(vec![symbol]),
209            Helper::Symbols(symbols) => Symbols::Symbols(symbols),
210        })
211    }
212}
213
214/// A struct for holding an API key that implements Debug, but will only print the last
215/// five characters of the key.
216#[derive(Clone)]
217pub struct ApiKey(String);
218
219pub(crate) const BUCKET_ID_LENGTH: usize = 5;
220
221impl fmt::Debug for ApiKey {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        write!(
224            f,
225            "\"…{}\"",
226            &self.0[self.0.len().saturating_sub(BUCKET_ID_LENGTH)..]
227        )
228    }
229}
230
231impl ApiKey {
232    /// Validates `key` meets requirements of an API key.
233    ///
234    /// # Errors
235    /// This function returns an error if the key is invalid.
236    pub fn new(key: String) -> crate::Result<ApiKey> {
237        if key == "$YOUR_API_KEY" {
238            Err(Error::bad_arg(
239                "key",
240                "got placeholder API key '$YOUR_API_KEY'. Please pass a real API key",
241            ))
242        } else if key.len() != API_KEY_LENGTH {
243            Err(Error::bad_arg(
244                "key",
245                format!(
246                    "expected to be 32-characters long, got {} characters",
247                    key.len()
248                ),
249            ))
250        } else if !key.is_ascii() {
251            error!("API key '{key}' contains non-ASCII characters");
252            Err(Error::bad_arg(
253                "key",
254                "expected to be composed of only ASCII characters",
255            ))
256        } else {
257            Ok(ApiKey(key))
258        }
259    }
260
261    /// Returns a slice of the last 5 characters of the key.
262    #[cfg(feature = "live")]
263    pub fn bucket_id(&self) -> &str {
264        // Safe to splice because validated as only containing ASCII characters in [`Self::new()`]
265        &self.0[API_KEY_LENGTH - BUCKET_ID_LENGTH..]
266    }
267
268    /// Returns the entire key as a slice.
269    pub fn as_str(&self) -> &str {
270        self.0.as_str()
271    }
272}
273
274#[cfg(test)]
275const TEST_DATA_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data");
276#[cfg(test)]
277pub(crate) fn zst_test_data_path(schema: dbn::enums::Schema) -> String {
278    format!("{TEST_DATA_PATH}/test_data.{}.dbn.zst", schema.as_str())
279}
280#[cfg(test)]
281pub(crate) fn body_contains(
282    key: impl Display,
283    val: impl Display,
284) -> wiremock::matchers::BodyContainsMatcher {
285    wiremock::matchers::body_string_contains(format!("{key}={val}"))
286}
287
288static USER_AGENT: LazyLock<String> = LazyLock::new(|| {
289    format!(
290        "Databento/{} Rust {}-{}",
291        env!("CARGO_PKG_VERSION"),
292        std::env::consts::OS,
293        std::env::consts::ARCH,
294    )
295});
296
297/// A datetime or object that can be non-fallibly converted to a datetime.
298pub trait DateTimeLike {
299    /// Converts the object to a datetime.
300    fn to_date_time(self) -> time::OffsetDateTime;
301}
302
303impl DateTimeLike for time::OffsetDateTime {
304    fn to_date_time(self) -> time::OffsetDateTime {
305        self
306    }
307}
308
309impl DateTimeLike for time::Date {
310    fn to_date_time(self) -> time::OffsetDateTime {
311        self.with_time(time::Time::MIDNIGHT).assume_utc()
312    }
313}
314
315#[cfg(feature = "chrono")]
316impl<Tz: chrono::TimeZone> DateTimeLike for chrono::DateTime<Tz> {
317    fn to_date_time(self) -> time::OffsetDateTime {
318        // timestamp_nanos_opt() returns None for dates outside ~1677-2262.
319        // from_unix_timestamp_nanos() fails for dates outside ~1000-9999.
320        // Practical timestamps fall well within these bounds, so unwrap is safe.
321        time::OffsetDateTime::from_unix_timestamp_nanos(
322            self.to_utc().timestamp_nanos_opt().unwrap() as i128,
323        )
324        .unwrap()
325    }
326}
327
328#[cfg(feature = "chrono")]
329impl DateTimeLike for chrono::NaiveDate {
330    fn to_date_time(self) -> time::OffsetDateTime {
331        use chrono::Datelike;
332        // Conversion is infallible for valid NaiveDate: month is 1-12, day is 1-31.
333        time::Date::from_calendar_date(
334            self.year(),
335            time::Month::try_from(self.month() as u8).unwrap(),
336            self.day() as u8,
337        )
338        .unwrap()
339        .with_time(time::Time::MIDNIGHT)
340        .assume_utc()
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_deserialize_symbols() {
350        const JSON: &str = r#"["ALL_SYMBOLS", [1, 2, 3], ["ESZ3", "CLZ3"], "TSLA", 1001]"#;
351        let symbol_res: Vec<Symbols> = serde_json::from_str(JSON).unwrap();
352        assert_eq!(symbol_res.len(), 5);
353        assert_eq!(symbol_res[0], Symbols::All);
354        assert_eq!(symbol_res[1], Symbols::Ids(vec![1, 2, 3]));
355        assert_eq!(
356            symbol_res[2],
357            Symbols::Symbols(vec!["ESZ3".to_owned(), "CLZ3".to_owned()])
358        );
359        assert_eq!(symbol_res[3], Symbols::Symbols(vec!["TSLA".to_owned()]));
360        assert_eq!(symbol_res[4], Symbols::Ids(vec![1001]));
361    }
362
363    #[test]
364    fn test_key_debug_truncates() {
365        assert_eq!(
366            format!("{:?}", ApiKey("abcdefghijklmnopqrstuvwxyz".to_owned())),
367            "\"…vwxyz\""
368        );
369    }
370
371    #[test]
372    fn test_key_debug_doesnt_underflow() {
373        assert_eq!(format!("{:?}", ApiKey("test".to_owned())), "\"…test\"");
374    }
375}