Skip to main content

nautilus_databento/
common.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Common functions to support Databento adapter operations.
17
18use std::{fmt::Debug, fs, path::Path, sync::LazyLock};
19
20use databento::historical::DateTimeRange;
21use indexmap::IndexMap;
22use nautilus_core::{UnixNanos, string::secret::REDACTED};
23use nautilus_model::identifiers::{ClientId, Venue};
24use time::OffsetDateTime;
25use ustr::Ustr;
26use zeroize::ZeroizeOnDrop;
27
28use crate::types::{DatabentoPublisher, PublisherId};
29
30/// Venue identifier string.
31pub const DATABENTO: &str = "DATABENTO";
32
33/// Static venue instance.
34pub static DATABENTO_VENUE: LazyLock<Venue> = LazyLock::new(|| Venue::new(Ustr::from(DATABENTO)));
35
36/// Static client ID instance.
37pub static DATABENTO_CLIENT_ID: LazyLock<ClientId> =
38    LazyLock::new(|| ClientId::new(Ustr::from(DATABENTO)));
39
40pub const ALL_SYMBOLS: &str = "ALL_SYMBOLS";
41
42/// Loads Databento publishers from a JSON file.
43///
44/// # Errors
45///
46/// Returns an error if the file cannot be read or parsed as JSON.
47pub fn load_publishers(filepath: impl AsRef<Path>) -> anyhow::Result<Vec<DatabentoPublisher>> {
48    let file_content = fs::read_to_string(filepath)?;
49    Ok(serde_json::from_str(&file_content)?)
50}
51
52/// Builds the publisher-to-venue map used by Databento decoders.
53#[must_use]
54pub fn build_publisher_venue_map(
55    publishers: &[DatabentoPublisher],
56) -> IndexMap<PublisherId, Venue> {
57    publishers
58        .iter()
59        .map(|p| (p.publisher_id, Venue::from(p.venue.as_str())))
60        .collect()
61}
62
63/// API credentials required for Databento API requests.
64#[derive(Clone, ZeroizeOnDrop)]
65pub struct Credential {
66    api_key: Box<[u8]>,
67}
68
69impl Debug for Credential {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct(stringify!(Credential))
72            .field("api_key", &REDACTED)
73            .finish()
74    }
75}
76
77impl Credential {
78    /// Creates a new [`Credential`] instance from the API key.
79    #[must_use]
80    pub fn new(api_key: impl Into<String>) -> Self {
81        let api_key_bytes = api_key.into().into_bytes();
82
83        Self {
84            api_key: api_key_bytes.into_boxed_slice(),
85        }
86    }
87
88    /// Returns the API key associated with this credential.
89    ///
90    /// # Panics
91    ///
92    /// This method should never panic as the API key is always valid UTF-8,
93    /// having been created from a String.
94    #[must_use]
95    pub fn api_key(&self) -> &str {
96        std::str::from_utf8(&self.api_key).expect("API key is valid UTF-8")
97    }
98
99    /// Returns a masked version of the API key for logging purposes.
100    ///
101    /// Shows first 4 and last 4 characters with ellipsis in between.
102    /// For keys shorter than 8 characters, shows asterisks only.
103    #[must_use]
104    pub fn api_key_masked(&self) -> String {
105        nautilus_core::string::secret::mask_api_key(self.api_key())
106    }
107}
108
109/// # Errors
110///
111/// Returns an error if converting `start` or `end` to `OffsetDateTime` fails.
112pub fn get_date_time_range(start: UnixNanos, end: UnixNanos) -> anyhow::Result<DateTimeRange> {
113    Ok(DateTimeRange::from((
114        OffsetDateTime::from_unix_timestamp_nanos(i128::from(start.as_u64()))?,
115        OffsetDateTime::from_unix_timestamp_nanos(i128::from(end.as_u64()))?,
116    )))
117}
118
119#[cfg(test)]
120mod tests {
121    use rstest::*;
122
123    use super::*;
124
125    #[rstest]
126    #[case(
127        UnixNanos::default(),
128        UnixNanos::default(),
129        "DateTimeRange { start: 1970-01-01 0:00:00.0 +00:00:00, end: 1970-01-01 0:00:00.0 +00:00:00 }"
130    )]
131    #[case(UnixNanos::default(), 1_000_000_000.into(), "DateTimeRange { start: 1970-01-01 0:00:00.0 +00:00:00, end: 1970-01-01 0:00:01.0 +00:00:00 }")]
132    fn test_get_date_time_range(
133        #[case] start: UnixNanos,
134        #[case] end: UnixNanos,
135        #[case] range_str: &str,
136    ) {
137        let range = get_date_time_range(start, end).unwrap();
138        assert_eq!(format!("{range:?}"), range_str);
139    }
140
141    #[rstest]
142    fn test_credential_api_key_masked_short() {
143        let credential = Credential::new("short");
144        assert_eq!(credential.api_key_masked(), "*****");
145    }
146
147    #[rstest]
148    fn test_credential_api_key_masked_long() {
149        let credential = Credential::new("abcdefghijklmnop");
150        assert_eq!(credential.api_key_masked(), "abcd...mnop");
151    }
152
153    #[rstest]
154    fn test_credential_debug_redaction() {
155        let credential = Credential::new("test_api_key");
156        let debug_str = format!("{credential:?}");
157        assert!(debug_str.contains(REDACTED));
158        assert!(!debug_str.contains("test_api_key"));
159    }
160}