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