amazon_spapi/client/
client.rs1use anyhow::Result;
2use reqwest::header::HeaderMap;
3use reqwest::Client;
4use serde::Deserialize;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::time::Duration;
8use tokio::sync::Mutex;
9
10use crate::apis::configuration::Configuration;
11use crate::client::{AuthClient, RateLimiter, Region, SpapiConfig};
12
13pub struct SpapiClient {
14 client: Client,
15 auth_client: Arc<Mutex<AuthClient>>,
16 config: SpapiConfig,
17 rate_limiter: RateLimiter,
18}
19
20impl SpapiClient {
21 pub fn new(config: SpapiConfig) -> Result<Self> {
23 let user_agent = if let Some(ua) = &config.user_agent {
24 ua.clone()
25 } else {
26 Self::get_default_user_agent()
28 };
29
30 let mut client_builder = Client::builder()
31 .timeout(std::time::Duration::from_secs(
32 config.timeout_sec.unwrap_or(30),
33 ))
34 .user_agent(&user_agent);
35
36 if let Some(proxy_url) = &config.proxy {
37 let proxy = reqwest::Proxy::all(proxy_url)?;
38 client_builder = client_builder.proxy(proxy);
39 }
40
41 let client = client_builder.build()?;
42
43 let auth_client = AuthClient::new(config.clone())?;
44
45 let rate_limiter =
47 RateLimiter::new_with_safety_factor(config.rate_limit_factor.unwrap_or(1.05));
48
49 Ok(Self {
50 client, auth_client: Arc::new(Mutex::new(auth_client)),
52 config,
53 rate_limiter,
54 })
55 }
56
57 pub fn limiter(&self) -> &RateLimiter {
59 &self.rate_limiter
60 }
61
62 pub fn get_default_user_agent() -> String {
64 let platform = format!("{}/{}", std::env::consts::OS, std::env::consts::ARCH);
65 format!(
66 "amazon-spapi/v{} (Language=Rust; Platform={})",
67 env!("CARGO_PKG_VERSION"),
68 platform
69 )
70 }
71
72 pub fn get_base_url(&self) -> String {
74 if self.config.sandbox {
75 match self.config.region {
76 Region::NorthAmerica => format!("https://sandbox.sellingpartnerapi-na.amazon.com"),
77 Region::Europe => format!("https://sandbox.sellingpartnerapi-eu.amazon.com"),
78 Region::FarEast => format!("https://sandbox.sellingpartnerapi-fe.amazon.com"),
79 }
80 } else {
81 match self.config.region {
82 Region::NorthAmerica => format!("https://sellingpartnerapi-na.amazon.com"),
83 Region::Europe => format!("https://sellingpartnerapi-eu.amazon.com"),
84 Region::FarEast => format!("https://sellingpartnerapi-fe.amazon.com"),
85 }
86 }
87 }
88
89 pub async fn get_access_token(&self) -> Result<String> {
91 let mut auth_client = self.auth_client.lock().await;
92 auth_client.get_access_token().await
93 }
94
95 pub fn is_sandbox(&self) -> bool {
97 self.config.sandbox
98 }
99
100 pub async fn upload(&self, url: &str, content: &str, content_type: &str) -> Result<()> {
102 let response = self
103 .client
104 .put(url)
105 .header("Content-Type", content_type)
106 .body(content.to_string())
107 .send()
108 .await?;
109
110 if response.status().is_success() {
111 log::info!("Feed document content uploaded successfully");
112 Ok(())
113 } else {
114 let status = response.status();
115 let error_text = response.text().await?;
116 Err(anyhow::anyhow!(
117 "Failed to upload feed document content: {} - Response: {}",
118 status,
119 error_text
120 ))
121 }
122 }
123
124 pub async fn download(&self, url: &str) -> Result<String> {
126 let response = self.get_http_client().get(url).send().await?;
127
128 if response.status().is_success() {
129 let content = response.text().await?;
130 log::info!("Feed document content downloaded successfully");
131 Ok(content)
132 } else {
133 let status = response.status();
134 let error_text = response.text().await?;
135 Err(anyhow::anyhow!(
136 "Failed to download feed document content: {} - Response: {}",
137 status,
138 error_text
139 ))
140 }
141 }
142
143 #[allow(unused)]
145 #[deprecated]
146 pub async fn get_rate_limit_status(&self) -> Result<HashMap<String, (f64, f64, u32)>> {
147 Ok(self.rate_limiter.get_token_status().await?)
148 }
149
150 #[allow(unused)]
152 #[deprecated]
153 pub async fn check_rate_limit_availability(&self, endpoint_id: &String) -> Result<bool> {
154 Ok(self
155 .rate_limiter
156 .check_token_availability(endpoint_id)
157 .await?)
158 }
159
160 pub async fn refresh_access_token_if_needed(&self) -> Result<()> {
162 let mut auth_client = self.auth_client.lock().await;
163 if !auth_client.is_token_valid() {
164 auth_client.refresh_access_token().await?;
165 }
166 Ok(())
167 }
168
169 pub async fn force_refresh_token(&self) -> Result<()> {
171 let mut auth_client = self.auth_client.lock().await;
172 auth_client.refresh_access_token().await?;
173 Ok(())
174 }
175
176 pub fn get_http_client(&self) -> &Client {
178 &self.client
179 }
180
181 pub async fn create_configuration(&self) -> Result<Configuration> {
184 let mut headers = reqwest::header::HeaderMap::new();
185 headers.insert("Content-Type", "application/json; charset=utf-8".parse()?);
186 headers.insert("host", "sellingpartnerapi-na.amazon.com".parse()?);
187 headers.insert(
188 "x-amz-access-token",
189 self.get_access_token().await?.parse()?,
190 );
191 headers.insert(
192 "x-amz-date",
193 {
194 let now = time::OffsetDateTime::now_utc();
195 format!(
196 "{:04}{:02}{:02}T{:02}{:02}{:02}Z",
197 now.year(),
198 now.month() as u8,
199 now.day(),
200 now.hour(),
201 now.minute(),
202 now.second()
203 )
204 }
205 .parse()?,
206 );
207 headers.insert(
208 "user-agent",
209 self.config
210 .user_agent
211 .clone()
212 .unwrap_or_else(|| Self::get_default_user_agent())
213 .parse()?,
214 );
215
216 let user_agent = if let Some(ua) = &self.config.user_agent {
217 ua.clone()
218 } else {
219 Self::get_default_user_agent()
221 };
222
223 let mut client_builder = Client::builder()
224 .timeout(std::time::Duration::from_secs(
225 self.config.timeout_sec.unwrap_or(30),
226 ))
227 .default_headers(headers)
228 .user_agent(&user_agent);
229
230 if let Some(proxy_url) = &self.config.proxy {
231 let proxy = reqwest::Proxy::all(proxy_url)?;
232 client_builder = client_builder.proxy(proxy);
233 }
234
235 let http_client = client_builder.build()?;
236
237 let configuration = Configuration {
238 base_path: self.get_base_url(),
239 client: crate::apis::configuration::CustomClient::new(http_client, self.config.retry_count.unwrap_or(0)),
240 user_agent: Some(
241 self.config
242 .user_agent
243 .clone()
244 .unwrap_or_else(|| Self::get_default_user_agent()),
245 ),
246 };
247 Ok(configuration)
248 }
249
250 pub fn from_json<'a, T>(s: &'a str) -> Result<T>
251 where
252 T: Deserialize<'a>,
253 {
254 serde_json::from_str(s).map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}: {}", e, s))
255 }
256}