runpod_sdk/client/runpod.rs
1//! RunPod API client implementation.
2//!
3//! This module contains the main [`RunpodClient`] struct and its implementation,
4//! providing the core HTTP client functionality for interacting with the RunPod API.
5
6use std::fmt;
7use std::sync::Arc;
8
9use reqwest::{Client, RequestBuilder};
10
11use super::config::RunpodConfig;
12use crate::Result;
13#[cfg(feature = "tracing")]
14use crate::TRACING_TARGET_CLIENT;
15use crate::builder::RunpodBuilder;
16
17/// Main RunPod API client for interacting with all RunPod services.
18///
19/// The `RunpodClient` provides access to all RunPod API endpoints through specialized
20/// service interfaces. It handles authentication, request/response serialization,
21/// and provides a consistent async interface for all operations.
22///
23/// # Features
24///
25/// - **Thread-safe**: Safe to use across multiple threads
26/// - **Cheap to clone**: Uses `Arc` internally for efficient cloning
27/// - **Automatic authentication**: Handles API key authentication automatically
28/// - **Comprehensive coverage**: Access to all RunPod services (Pods, Endpoints, Templates, etc.)
29///
30/// # Services
31///
32/// The client implements V1 API service traits that provide direct access to API methods:
33///
34/// - [`PodsService`](crate::service::PodsService) - Pod lifecycle management
35/// - [`EndpointsService`](crate::service::EndpointsService) - Serverless endpoint operations
36/// - [`TemplatesService`](crate::service::TemplatesService) - Template creation and management
37/// - [`VolumesService`](crate::service::VolumesService) - Network volume operations
38/// - [`RegistryService`](crate::service::RegistryService) - Registry authentication
39/// - [`BillingService`](crate::service::BillingService) - Usage and billing information
40///
41/// # Examples
42///
43/// ## Basic usage with environment configuration
44///
45/// ```no_run
46/// use runpod_sdk::{RunpodClient, Result};
47/// use runpod_sdk::model::ListPodsQuery;
48/// use runpod_sdk::service::PodsService;
49///
50/// # async fn example() -> Result<()> {
51/// let client = RunpodClient::from_env()?;
52///
53/// // List all pods
54/// let pods = client.list_pods(ListPodsQuery::default()).await?;
55/// println!("Found {} pods", pods.len());
56/// # Ok(())
57/// # }
58/// ```
59///
60/// ## Custom configuration with builder pattern
61///
62/// ```no_run
63/// use runpod_sdk::{RunpodConfig, RunpodClient, Result};
64/// use runpod_sdk::service::{PodsService, EndpointsService, TemplatesService};
65/// use std::time::Duration;
66///
67/// # async fn example() -> Result<()> {
68/// let client = RunpodConfig::builder()
69/// .with_api_key("your-api-key")
70/// .with_rest_url("https://rest.runpod.io/v1")
71/// .with_timeout(Duration::from_secs(30))
72/// .build_client()?;
73///
74/// // Use different services
75/// let pods = client.list_pods(Default::default()).await?;
76/// let endpoints = client.list_endpoints(Default::default()).await?;
77/// let templates = client.list_templates(Default::default()).await?;
78/// # Ok(())
79/// # }
80/// ```
81///
82/// ## Multi-threaded usage
83///
84/// The client is cheap to clone (uses `Arc` internally):
85///
86/// ```no_run
87/// use runpod_sdk::{RunpodClient, Result};
88/// use runpod_sdk::service::PodsService;
89/// use tokio::task;
90///
91/// # async fn example() -> Result<()> {
92/// let client = RunpodClient::from_env()?;
93///
94/// let handles: Vec<_> = (0..3).map(|i| {
95/// let client = client.clone();
96/// task::spawn(async move {
97/// let pods = client.list_pods(Default::default()).await?;
98/// println!("Thread {}: Found {} pods", i, pods.len());
99/// Ok::<(), runpod_sdk::Error>(())
100/// })
101/// }).collect();
102///
103/// for handle in handles {
104/// handle.await.unwrap()?;
105/// }
106/// # Ok(())
107/// # }
108/// ```
109#[derive(Clone)]
110pub struct RunpodClient {
111 pub(crate) inner: Arc<RunpodClientInner>,
112}
113
114/// Inner client state that is shared via Arc for cheap cloning.
115#[derive(Debug)]
116pub(crate) struct RunpodClientInner {
117 pub(crate) config: RunpodConfig,
118 pub(crate) client: Client,
119}
120
121impl RunpodClient {
122 /// Creates a new Runpod API client.
123 #[cfg_attr(
124 feature = "tracing",
125 tracing::instrument(
126 skip(config),
127 target = TRACING_TARGET_CLIENT,
128 fields(api_key = %config.masked_api_key())
129 )
130 )]
131 pub fn new(config: RunpodConfig) -> Result<Self> {
132 #[cfg(feature = "tracing")]
133 tracing::debug!(target: TRACING_TARGET_CLIENT, "Creating RunPod client");
134
135 let client = if let Some(custom_client) = config.client() {
136 custom_client
137 } else {
138 Client::builder().timeout(config.timeout()).build()?
139 };
140
141 #[cfg(feature = "tracing")]
142 tracing::info!(target: TRACING_TARGET_CLIENT,
143 rest_url = %config.rest_url(),
144 timeout = ?config.timeout(),
145 api_key = %config.masked_api_key(),
146 custom_client = config.client().is_some(),
147 "RunPod client created successfully"
148 );
149
150 let inner = Arc::new(RunpodClientInner { config, client });
151 Ok(Self { inner })
152 }
153
154 /// Makes a GET request to the API endpoint URL (not GraphQL).
155 ///
156 /// This is a low-level method for making GET requests to the RunPod API.
157 /// The path should be relative to the API base URL (e.g., "endpoint_id/status/job_id").
158 #[cfg(feature = "serverless")]
159 #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
160 #[cfg_attr(
161 feature = "tracing",
162 tracing::instrument(
163 skip(self),
164 target = TRACING_TARGET_CLIENT,
165 fields(method = "GET", path, url)
166 )
167 )]
168 pub(crate) fn get_api(&self, path: &str) -> RequestBuilder {
169 let url = format!("{}/{}", self.inner.config.api_url(), path);
170
171 #[cfg(feature = "tracing")]
172 tracing::trace!(target: TRACING_TARGET_CLIENT,
173 url = %url,
174 method = "GET",
175 "Creating HTTP GET request to API"
176 );
177
178 self.inner
179 .client
180 .get(&url)
181 .bearer_auth(self.inner.config.api_key())
182 .timeout(self.inner.config.timeout())
183 }
184
185 /// Makes a POST request to the API endpoint URL (not GraphQL).
186 ///
187 /// This is a low-level method for making POST requests to the RunPod API.
188 /// The path should be relative to the API base URL (e.g., "endpoint_id/run").
189 #[cfg(feature = "serverless")]
190 #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
191 #[cfg_attr(
192 feature = "tracing",
193 tracing::instrument(
194 skip(self),
195 target = TRACING_TARGET_CLIENT,
196 fields(method = "POST", path, url)
197 )
198 )]
199 pub(crate) fn post_api(&self, path: &str) -> RequestBuilder {
200 let url = format!("{}/{}", self.inner.config.api_url(), path);
201
202 #[cfg(feature = "tracing")]
203 tracing::trace!(target: TRACING_TARGET_CLIENT,
204 url = %url,
205 method = "POST",
206 "Creating HTTP POST request to API"
207 );
208
209 self.inner
210 .client
211 .post(&url)
212 .bearer_auth(self.inner.config.api_key())
213 .timeout(self.inner.config.timeout())
214 }
215
216 /// Creates a GET request.
217 #[cfg_attr(
218 feature = "tracing",
219 tracing::instrument(
220 skip(self),
221 target = TRACING_TARGET_CLIENT,
222 fields(method = "GET", path, url)
223 )
224 )]
225 pub(crate) fn get(&self, path: &str) -> RequestBuilder {
226 let url = format!("{}{}", self.inner.config.rest_url(), path);
227
228 #[cfg(feature = "tracing")]
229 tracing::trace!(target: TRACING_TARGET_CLIENT,
230 url = %url,
231 method = "GET",
232 "Creating HTTP GET request"
233 );
234
235 self.inner
236 .client
237 .get(&url)
238 .bearer_auth(self.inner.config.api_key())
239 .timeout(self.inner.config.timeout())
240 }
241
242 /// Creates a POST request.
243 #[cfg_attr(
244 feature = "tracing",
245 tracing::instrument(
246 skip(self),
247 target = TRACING_TARGET_CLIENT,
248 fields(method = "POST", path, url)
249 )
250 )]
251 pub(crate) fn post(&self, path: &str) -> RequestBuilder {
252 let url = format!("{}{}", self.inner.config.rest_url(), path);
253
254 #[cfg(feature = "tracing")]
255 tracing::trace!(target: TRACING_TARGET_CLIENT,
256 url = %url,
257 method = "POST",
258 "Creating HTTP POST request"
259 );
260
261 self.inner
262 .client
263 .post(&url)
264 .bearer_auth(self.inner.config.api_key())
265 .timeout(self.inner.config.timeout())
266 }
267
268 /// Creates a PATCH request.
269 #[cfg_attr(
270 feature = "tracing",
271 tracing::instrument(
272 skip(self),
273 target = TRACING_TARGET_CLIENT,
274 fields(method = "PATCH", path, url)
275 )
276 )]
277 pub(crate) fn patch(&self, path: &str) -> RequestBuilder {
278 let url = format!("{}{}", self.inner.config.rest_url(), path);
279
280 #[cfg(feature = "tracing")]
281 tracing::trace!(target: TRACING_TARGET_CLIENT,
282 url = %url,
283 method = "PATCH",
284 "Creating HTTP PATCH request"
285 );
286
287 self.inner
288 .client
289 .patch(&url)
290 .bearer_auth(self.inner.config.api_key())
291 .timeout(self.inner.config.timeout())
292 }
293
294 /// Creates a DELETE request.
295 #[cfg_attr(
296 feature = "tracing",
297 tracing::instrument(
298 skip(self),
299 target = TRACING_TARGET_CLIENT,
300 fields(method = "DELETE", path, url)
301 )
302 )]
303 pub(crate) fn delete(&self, path: &str) -> RequestBuilder {
304 let url = format!("{}{}", self.inner.config.rest_url(), path);
305
306 #[cfg(feature = "tracing")]
307 tracing::trace!(target: TRACING_TARGET_CLIENT,
308 url = %url,
309 method = "DELETE",
310 "Creating HTTP DELETE request"
311 );
312
313 self.inner
314 .client
315 .delete(&url)
316 .bearer_auth(self.inner.config.api_key())
317 .timeout(self.inner.config.timeout())
318 }
319
320 /// Creates a new configuration builder for constructing a RunPod client.
321 ///
322 /// This is a convenience method that returns a `RunpodConfigBuilder` for building
323 /// a custom client configuration.
324 ///
325 /// # Example
326 /// ```no_run
327 /// # use runpod_sdk::{RunpodClient, Result};
328 /// # use std::time::Duration;
329 /// # async fn example() -> Result<()> {
330 /// let client = RunpodClient::builder()
331 /// .with_api_key("your-api-key")
332 /// .with_timeout(Duration::from_secs(60))
333 /// .build_client()?;
334 /// # Ok(())
335 /// # }
336 /// ```
337 pub fn builder() -> RunpodBuilder {
338 RunpodConfig::builder()
339 }
340
341 /// Creates a new Runpod API client from environment variables.
342 ///
343 /// This is a convenience method that creates a RunpodConfig from environment
344 /// variables and then creates a client from that config.
345 ///
346 /// # Environment Variables
347 ///
348 /// - `RUNPOD_API_KEY` - Your RunPod API key (required)
349 /// - `RUNPOD_BASE_URL` - Base URL for the API (optional, defaults to <https://rest.runpod.io/v1>)
350 /// - `RUNPOD_TIMEOUT_SECS` - Request timeout in seconds (optional, defaults to 30)
351 ///
352 /// # Example
353 /// ```no_run
354 /// # use runpod_sdk::{RunpodClient, Result};
355 /// # async fn example() -> Result<()> {
356 /// let client = RunpodClient::from_env()?;
357 /// # Ok(())
358 /// # }
359 /// ```
360 #[cfg_attr(feature = "tracing", tracing::instrument(target = TRACING_TARGET_CLIENT))]
361 pub fn from_env() -> Result<Self> {
362 #[cfg(feature = "tracing")]
363 tracing::debug!(target: TRACING_TARGET_CLIENT, "Creating RunPod client from environment");
364
365 let config = RunpodConfig::from_env()?;
366 Self::new(config)
367 }
368}
369
370impl fmt::Debug for RunpodClient {
371 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372 let mut debug_struct = f.debug_struct("RunpodClient");
373 debug_struct
374 .field("api_key", &self.inner.config.masked_api_key())
375 .field("rest_url", &self.inner.config.rest_url())
376 .field("timeout", &self.inner.config.timeout());
377
378 #[cfg(feature = "serverless")]
379 debug_struct.field("api_url", &self.inner.config.api_url());
380 debug_struct.finish()
381 }
382}