Skip to main content

alpaca_data/
client.rs

1use std::fmt;
2use std::sync::Arc;
3use std::time::Duration;
4
5use alpaca_core::{BaseUrl, Credentials, env};
6use alpaca_http::{
7    ConcurrencyLimit, HttpClient, HttpResponse, NoContent, RequestParts, RetryConfig,
8    StaticHeaderAuthenticator, TransportObserver,
9};
10use serde::de::DeserializeOwned;
11
12use crate::{
13    Error, corporate_actions::CorporateActionsClient, crypto::CryptoClient, news::NewsClient,
14    options::OptionsClient, stocks::StocksClient,
15};
16
17pub const DATA_API_KEY_ENV: &str = "ALPACA_DATA_API_KEY";
18pub const DATA_SECRET_KEY_ENV: &str = "ALPACA_DATA_SECRET_KEY";
19const DEFAULT_DATA_BASE_URL: &str = "https://data.alpaca.markets";
20const APCA_API_KEY_HEADER: &str = "APCA-API-KEY-ID";
21const APCA_API_SECRET_HEADER: &str = "APCA-API-SECRET-KEY";
22const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
23const DEFAULT_MAX_IN_FLIGHT: usize = 50;
24const DEFAULT_POOL_MAX_IDLE_PER_HOST: usize = 50;
25const DEFAULT_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
26const DEFAULT_TCP_KEEPALIVE: Duration = Duration::from_secs(60);
27
28#[derive(Clone)]
29pub struct Client {
30    pub(crate) inner: Arc<ClientInner>,
31}
32
33#[allow(dead_code)]
34pub(crate) struct ClientInner {
35    http: HttpClient,
36    auth: StaticHeaderAuthenticator,
37    base_url: BaseUrl,
38}
39
40#[derive(Clone, Default)]
41pub struct ClientBuilder {
42    api_key: Option<String>,
43    secret_key: Option<String>,
44    base_url: Option<BaseUrl>,
45    timeout: Option<Duration>,
46    observer: Option<Arc<dyn TransportObserver>>,
47    retry_config: RetryConfig,
48    max_in_flight: Option<usize>,
49}
50
51impl Client {
52    #[must_use]
53    pub fn builder() -> ClientBuilder {
54        ClientBuilder::default()
55    }
56
57    pub fn new(credentials: Credentials) -> Result<Self, Error> {
58        Self::builder().credentials(credentials).build()
59    }
60
61    pub fn from_env() -> Result<Self, Error> {
62        Self::builder().credentials_from_env()?.build()
63    }
64
65    #[must_use]
66    pub fn base_url(&self) -> &BaseUrl {
67        &self.inner.base_url
68    }
69
70    #[must_use]
71    pub fn stocks(&self) -> StocksClient {
72        StocksClient::new(self.inner.clone())
73    }
74
75    #[must_use]
76    pub fn crypto(&self) -> CryptoClient {
77        CryptoClient::new(self.inner.clone())
78    }
79
80    #[must_use]
81    pub fn options(&self) -> OptionsClient {
82        OptionsClient::new(self.inner.clone())
83    }
84
85    #[must_use]
86    pub fn news(&self) -> NewsClient {
87        NewsClient::new(self.inner.clone())
88    }
89
90    #[must_use]
91    pub fn corporate_actions(&self) -> CorporateActionsClient {
92        CorporateActionsClient::new(self.inner.clone())
93    }
94}
95
96impl fmt::Debug for Client {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.debug_struct("Client")
99            .field("base_url", self.base_url())
100            .field("http", &"HttpClient")
101            .field("auth", &"[REDACTED]")
102            .finish()
103    }
104}
105
106impl fmt::Debug for ClientInner {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.debug_struct("ClientInner")
109            .field("base_url", &self.base_url)
110            .field("http", &"HttpClient")
111            .field("auth", &"[REDACTED]")
112            .finish()
113    }
114}
115
116impl fmt::Debug for ClientBuilder {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.debug_struct("ClientBuilder")
119            .field("api_key", &redacted_option(&self.api_key))
120            .field("secret_key", &redacted_option(&self.secret_key))
121            .field("base_url", &self.base_url)
122            .field("timeout", &self.timeout)
123            .field(
124                "observer",
125                &self.observer.as_ref().map(|_| "TransportObserver"),
126            )
127            .field("retry_config", &self.retry_config)
128            .field("max_in_flight", &self.max_in_flight)
129            .finish()
130    }
131}
132
133impl ClientBuilder {
134    #[must_use]
135    pub fn credentials(mut self, credentials: Credentials) -> Self {
136        self.api_key = Some(credentials.api_key().to_owned());
137        self.secret_key = Some(credentials.secret_key().to_owned());
138        self
139    }
140
141    #[must_use]
142    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
143        self.api_key = Some(api_key.into());
144        self
145    }
146
147    #[must_use]
148    pub fn secret_key(mut self, secret_key: impl Into<String>) -> Self {
149        self.secret_key = Some(secret_key.into());
150        self
151    }
152
153    #[must_use]
154    pub fn base_url(mut self, base_url: BaseUrl) -> Self {
155        self.base_url = Some(base_url);
156        self
157    }
158
159    pub fn base_url_str(mut self, base_url: impl AsRef<str>) -> Result<Self, Error> {
160        self.base_url = Some(BaseUrl::new(base_url.as_ref())?);
161        Ok(self)
162    }
163
164    pub fn credentials_from_env(self) -> Result<Self, Error> {
165        self.credentials_from_env_names(DATA_API_KEY_ENV, DATA_SECRET_KEY_ENV)
166    }
167
168    pub fn credentials_from_env_names(
169        mut self,
170        api_key_var: &str,
171        secret_key_var: &str,
172    ) -> Result<Self, Error> {
173        if let Some(credentials) = env::credentials_from_env_names(api_key_var, secret_key_var)? {
174            return Ok(self.credentials(credentials));
175        }
176
177        if let Some(credentials) = env::credentials_from_env()? {
178            self = self.credentials(credentials);
179        }
180
181        Ok(self)
182    }
183
184    #[must_use]
185    pub fn timeout(mut self, timeout: Duration) -> Self {
186        self.timeout = Some(timeout);
187        self
188    }
189
190    #[must_use]
191    pub fn observer(mut self, observer: Arc<dyn TransportObserver>) -> Self {
192        self.observer = Some(observer);
193        self
194    }
195
196    #[must_use]
197    pub fn retry_config(mut self, retry_config: RetryConfig) -> Self {
198        self.retry_config = retry_config;
199        self
200    }
201
202    #[must_use]
203    pub fn max_in_flight(mut self, max_in_flight: usize) -> Self {
204        self.max_in_flight = Some(max_in_flight);
205        self
206    }
207
208    pub fn build(self) -> Result<Client, Error> {
209        let credentials = match (self.api_key, self.secret_key) {
210            (Some(api_key), Some(secret_key)) => Credentials::new(api_key, secret_key)?,
211            (None, None) => return Err(Error::MissingCredentials),
212            _ => {
213                return Err(Error::InvalidConfiguration(
214                    "api_key and secret_key must be paired".to_owned(),
215                ));
216            }
217        };
218
219        let base_url = match self.base_url {
220            Some(base_url) => base_url,
221            None => BaseUrl::new(DEFAULT_DATA_BASE_URL)?,
222        };
223        let auth = StaticHeaderAuthenticator::from_pairs([
224            (APCA_API_KEY_HEADER, credentials.api_key()),
225            (APCA_API_SECRET_HEADER, credentials.secret_key()),
226        ])?;
227
228        let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
229        let reqwest_client = Self::build_reqwest_client(timeout)?;
230
231        let mut http_builder = HttpClient::builder()
232            .retry_config(self.retry_config)
233            .reqwest_client(reqwest_client);
234        if let Some(observer) = self.observer {
235            http_builder = http_builder.observer(observer);
236        }
237        http_builder = http_builder.concurrency_limit(ConcurrencyLimit::new(Some(
238            self.max_in_flight.unwrap_or(DEFAULT_MAX_IN_FLIGHT),
239        )));
240
241        let http = http_builder.build()?;
242
243        Ok(Client {
244            inner: Arc::new(ClientInner {
245                http,
246                auth,
247                base_url,
248            }),
249        })
250    }
251
252    fn build_reqwest_client(timeout: Duration) -> Result<reqwest::Client, Error> {
253        reqwest::Client::builder()
254            .no_proxy()
255            .pool_max_idle_per_host(DEFAULT_POOL_MAX_IDLE_PER_HOST)
256            .pool_idle_timeout(DEFAULT_POOL_IDLE_TIMEOUT)
257            .tcp_keepalive(DEFAULT_TCP_KEEPALIVE)
258            .timeout(timeout)
259            .http1_only()
260            .build()
261            .map_err(|error| alpaca_http::Error::from_reqwest(error, None).into())
262    }
263}
264
265impl ClientInner {
266    #[allow(dead_code)]
267    pub(crate) async fn send_json<T>(&self, request: RequestParts) -> Result<HttpResponse<T>, Error>
268    where
269        T: DeserializeOwned,
270    {
271        self.http
272            .send_json(&self.base_url, request, Some(&self.auth))
273            .await
274            .map_err(Error::from)
275    }
276
277    #[allow(dead_code)]
278    pub(crate) async fn send_text(
279        &self,
280        request: RequestParts,
281    ) -> Result<HttpResponse<String>, Error> {
282        self.http
283            .send_text(&self.base_url, request, Some(&self.auth))
284            .await
285            .map_err(Error::from)
286    }
287
288    #[allow(dead_code)]
289    pub(crate) async fn send_no_content(
290        &self,
291        request: RequestParts,
292    ) -> Result<HttpResponse<NoContent>, Error> {
293        self.http
294            .send_no_content(&self.base_url, request, Some(&self.auth))
295            .await
296            .map_err(Error::from)
297    }
298}
299
300fn redacted_option(value: &Option<String>) -> &'static str {
301    match value {
302        Some(_) => "[REDACTED]",
303        None => "None",
304    }
305}