nautilus_databento/
common.rs1use 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
30pub const DATABENTO: &str = "DATABENTO";
32
33pub static DATABENTO_VENUE: LazyLock<Venue> = LazyLock::new(|| Venue::new(Ustr::from(DATABENTO)));
35
36pub static DATABENTO_CLIENT_ID: LazyLock<ClientId> =
38 LazyLock::new(|| ClientId::new(Ustr::from(DATABENTO)));
39
40pub const ALL_SYMBOLS: &str = "ALL_SYMBOLS";
41
42pub 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#[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#[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 #[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 #[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 #[must_use]
104 pub fn api_key_masked(&self) -> String {
105 nautilus_core::string::secret::mask_api_key(self.api_key())
106 }
107}
108
109pub 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}