Skip to main content

jams_client/
http.rs

1use crate::common::{get_url, GetModelsResponse, Predictions};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use std::time;
5
6#[derive(Serialize)]
7struct PredictRequest {
8    model_name: String,
9    input: String,
10}
11
12#[derive(Deserialize)]
13struct PredictResponse {
14    output: String,
15}
16
17#[derive(Serialize)]
18struct AddModelRequest {
19    model_name: String,
20}
21
22#[derive(Serialize)]
23struct UpdateModelRequest {
24    model_name: String,
25}
26
27#[async_trait]
28pub trait Client {
29    async fn health_check(&self) -> anyhow::Result<()>;
30    async fn predict(&self, model_name: String, model_input: String)
31        -> anyhow::Result<Predictions>;
32    async fn add_model(&self, model_name: String) -> anyhow::Result<()>;
33    async fn update_model(&self, model_name: String) -> anyhow::Result<()>;
34    async fn delete_model(&self, model_name: String) -> anyhow::Result<()>;
35    async fn get_models(&self) -> anyhow::Result<GetModelsResponse>;
36}
37
38pub struct ApiClient {
39    client: reqwest::Client,
40    base_url: String,
41    timeout: time::Duration,
42}
43
44impl ApiClient {
45    pub fn builder() -> ApiClientBuilder {
46        ApiClientBuilder::default()
47    }
48}
49
50#[derive(Default)]
51pub struct ApiClientBuilder {
52    base_url: String,
53    timeout: time::Duration,
54}
55
56impl ApiClientBuilder {
57    pub fn new(base_url: String) -> ApiClientBuilder {
58        ApiClientBuilder {
59            base_url: get_url(base_url),
60            timeout: time::Duration::from_secs(5),
61        }
62    }
63
64    pub fn with_timeout(mut self, timeout: u64) -> ApiClientBuilder {
65        self.timeout = time::Duration::from_secs(timeout);
66        self
67    }
68
69    pub fn build(self) -> anyhow::Result<ApiClient> {
70        let client = match reqwest::Client::builder().build() {
71            Ok(client) => client,
72            Err(err) => {
73                anyhow::bail!("failed to create reqwest client: {}", err)
74            }
75        };
76        Ok(ApiClient {
77            client,
78            base_url: self.base_url,
79            timeout: self.timeout,
80        })
81    }
82}
83
84#[async_trait]
85impl Client for ApiClient {
86    async fn health_check(&self) -> anyhow::Result<()> {
87        let url = format!("{}/{}", self.base_url, "healthcheck");
88        match self.client.get(url).timeout(self.timeout).send().await {
89            Ok(resp) => match resp.status().is_success() {
90                true => Ok(()),
91                false => {
92                    anyhow::bail!(
93                        "failed to health check J.A.M.S server ❌: {}",
94                        resp.text().await.unwrap()
95                    )
96                }
97            },
98            Err(err) => {
99                anyhow::bail!("failed to health check J.A.M.S server ❌: {}", err)
100            }
101        }
102    }
103
104    async fn predict(
105        &self,
106        model_name: String,
107        model_input: String,
108    ) -> anyhow::Result<Predictions> {
109        let url = format!("{}/{}", self.base_url, "api/predict");
110        match self
111            .client
112            .post(url)
113            .json(&PredictRequest {
114                model_name,
115                input: model_input,
116            })
117            .timeout(self.timeout)
118            .send()
119            .await
120        {
121            Ok(resp) => match resp.status().is_success() {
122                true => {
123                    let predictions = resp.json::<PredictResponse>().await?;
124                    match Predictions::from_bytes(predictions.output.as_ref()) {
125                        Ok(predictions) => Ok(predictions),
126                        Err(err) => {
127                            anyhow::bail!(
128                                "failed to parse response from bytes ❌: {}",
129                                err.to_string()
130                            )
131                        }
132                    }
133                }
134                false => {
135                    anyhow::bail!(
136                        "failed to get predictions ❌: {}",
137                        resp.text().await.unwrap()
138                    )
139                }
140            },
141            Err(err) => {
142                anyhow::bail!("failed to make predict request ❌: {}", err.to_string())
143            }
144        }
145    }
146
147    async fn add_model(&self, model_name: String) -> anyhow::Result<()> {
148        let url = format!("{}/{}", self.base_url, "api/models");
149        match self
150            .client
151            .post(url)
152            .json(&AddModelRequest { model_name })
153            .timeout(self.timeout)
154            .send()
155            .await
156        {
157            Ok(resp) => match resp.status().is_success() {
158                true => Ok(()),
159                false => {
160                    anyhow::bail!("failed to add model ❌: {}", resp.text().await.unwrap())
161                }
162            },
163            Err(err) => {
164                anyhow::bail!("failed to make add_model request ❌: {}", err.to_string())
165            }
166        }
167    }
168
169    async fn update_model(&self, model_name: String) -> anyhow::Result<()> {
170        let url = format!("{}/{}", self.base_url, "api/models");
171        match self
172            .client
173            .put(url)
174            .json(&UpdateModelRequest { model_name })
175            .timeout(self.timeout)
176            .send()
177            .await
178        {
179            Ok(resp) => match resp.status().is_success() {
180                true => Ok(()),
181                false => {
182                    anyhow::bail!("failed to update model ❌: {}", resp.text().await.unwrap())
183                }
184            },
185            Err(err) => {
186                anyhow::bail!(
187                    "failed to make update_model request ❌: {}",
188                    err.to_string()
189                )
190            }
191        }
192    }
193
194    async fn delete_model(&self, model_name: String) -> anyhow::Result<()> {
195        let url = format!(
196            "{}/{}?model_name={}",
197            self.base_url, "api/models", model_name
198        );
199        match self.client.delete(url).timeout(self.timeout).send().await {
200            Ok(resp) => match resp.status().is_success() {
201                true => Ok(()),
202                false => {
203                    anyhow::bail!("failed to delete model ❌: {}", resp.text().await.unwrap())
204                }
205            },
206            Err(err) => {
207                anyhow::bail!(
208                    "failed to make delete_model request ❌: {}",
209                    err.to_string()
210                )
211            }
212        }
213    }
214
215    async fn get_models(&self) -> anyhow::Result<GetModelsResponse> {
216        let url = format!("{}/{}", self.base_url, "api/models");
217        match self.client.get(url).timeout(self.timeout).send().await {
218            Ok(resp) => match resp.status().is_success() {
219                true => Ok(resp.json::<GetModelsResponse>().await?),
220                false => {
221                    anyhow::bail!("failed to get models ❌: {}", resp.text().await.unwrap())
222                }
223            },
224            Err(err) => {
225                anyhow::bail!("failed to make get_models request ❌: {}", err.to_string())
226            }
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use std::env;
235    fn get_url() -> String {
236        let hostname = env::var("JAMS_HTTP_HOSTNAME").unwrap_or("0.0.0.0".to_string());
237        format!("{}:3000", hostname)
238    }
239
240    #[tokio::test]
241    async fn successfully_sends_health_check_request() {
242        // Arrange
243        let client = ApiClientBuilder::new(get_url())
244            .with_timeout(2)
245            .build()
246            .unwrap();
247
248        // Act
249        let resp = client.health_check().await;
250
251        // Assert
252        assert!(resp.is_ok())
253    }
254
255    #[tokio::test]
256    async fn successfully_sends_get_model_request() {
257        // Arrange
258        let client = ApiClientBuilder::new(get_url())
259            .with_timeout(2)
260            .build()
261            .unwrap();
262
263        // Act
264        let result = client.get_models().await;
265
266        // Assert
267        assert!(result.is_ok());
268        let res = result.unwrap();
269        // We will have some models as we are loading from assets/model_store
270        assert!(!res.models.is_empty());
271    }
272
273    #[tokio::test]
274    async fn successfully_sends_delete_model_request() {
275        // Arrange
276        let client = ApiClientBuilder::new(get_url())
277            .with_timeout(2)
278            .build()
279            .unwrap();
280
281        // Act
282        client
283            .add_model("pytorch-my_awesome_californiahousing_model".to_string())
284            .await
285            .unwrap();
286
287        let resp = client
288            .delete_model("my_awesome_californiahousing_model".to_string())
289            .await;
290
291        // Assert
292        assert!(resp.is_ok())
293    }
294
295    #[tokio::test]
296    async fn successfully_sends_add_model_request() {
297        // Arrange
298        let client = ApiClientBuilder::new(get_url())
299            .with_timeout(2)
300            .build()
301            .unwrap();
302
303        // Act
304        client
305            .delete_model("my_awesome_penguin_model".to_string())
306            .await
307            .unwrap();
308
309        let resp = client
310            .add_model("tensorflow-my_awesome_penguin_model".to_string())
311            .await;
312
313        // Assert
314        assert!(resp.is_ok())
315    }
316
317    #[tokio::test]
318    async fn successfully_sends_update_model_request() {
319        // Arrange
320        let client = ApiClientBuilder::new(get_url())
321            .with_timeout(2)
322            .build()
323            .unwrap();
324
325        // Act
326        let resp = client.update_model("titanic_model".to_string()).await;
327
328        // Assert
329        assert!(resp.is_ok())
330    }
331
332    #[tokio::test]
333    async fn successfully_sends_predict_model_request() {
334        // Arrange
335        let client = ApiClientBuilder::new(get_url())
336            .with_timeout(2)
337            .build()
338            .unwrap();
339
340        // Act
341        let model_name = "titanic_model".to_string();
342        let model_input = serde_json::json!(
343                {
344                    "pclass": ["1", "3"],
345                    "sex": ["male", "female"],
346                    "age": [22.0, 23.79929292929293],
347                    "sibsp": ["0", "1", ],
348                    "parch": ["0", "0"],
349                    "fare": [151.55, 14.4542],
350                    "embarked": ["S", "C"],
351                    "class": ["First", "Third"],
352                    "who": ["man", "woman"],
353                    "adult_male": ["True", "False"],
354                    "deck": ["Unknown", "Unknown"],
355                    "embark_town": ["Southampton", "Cherbourg"],
356                    "alone": ["True", "False"]
357                }
358        )
359        .to_string();
360        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
361        let resp = client.predict(model_name, model_input).await;
362
363        // Assert
364        assert!(resp.is_ok())
365    }
366}