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 /// Executes a GraphQL query.
320 ///
321 /// # Arguments
322 ///
323 /// * `query` - The GraphQL query string
324 ///
325 /// # Returns
326 ///
327 /// Returns the deserialized response data of type `T`.
328 ///
329 /// # Example
330 /// ```no_run
331 /// # use runpod_sdk::{RunpodClient, Result};
332 /// # use serde::Deserialize;
333 /// # #[derive(Deserialize)]
334 /// # struct MyResponse {
335 /// # data: String,
336 /// # }
337 /// # async fn example() -> Result<()> {
338 /// let client = RunpodClient::from_env()?;
339 /// let query = r#"{ viewer { id name } }"#;
340 /// let response: MyResponse = client.graphql_query(query).await?;
341 /// # Ok(())
342 /// # }
343 /// ```
344 #[cfg(feature = "graphql")]
345 #[cfg_attr(docsrs, doc(cfg(feature = "graphql")))]
346 #[cfg_attr(
347 feature = "tracing",
348 tracing::instrument(
349 skip(self, query),
350 target = TRACING_TARGET_CLIENT,
351 fields(query_len = query.len(), url, status)
352 )
353 )]
354 pub async fn graphql_query<T>(&self, query: &str) -> Result<T>
355 where
356 T: for<'de> serde::Deserialize<'de>,
357 {
358 let url = self.inner.config.graphql_url();
359
360 #[cfg(feature = "tracing")]
361 tracing::debug!(target: TRACING_TARGET_CLIENT,
362 url = %url,
363 query_len = query.len(),
364 api_key = %self.inner.config.masked_api_key(),
365 "Executing GraphQL query"
366 );
367
368 let request = self
369 .inner
370 .client
371 .post(url)
372 .bearer_auth(self.inner.config.api_key())
373 .timeout(self.inner.config.timeout())
374 .json(&serde_json::json!({ "query": query }));
375
376 let response = request.send().await?;
377 let status = response.status();
378
379 #[cfg(feature = "tracing")]
380 tracing::debug!(target: TRACING_TARGET_CLIENT,
381 status = %status,
382 success = status.is_success(),
383 "GraphQL response received"
384 );
385
386 let result = response.json().await?;
387 Ok(result)
388 }
389
390 /// Creates a new configuration builder for constructing a RunPod client.
391 ///
392 /// This is a convenience method that returns a `RunpodConfigBuilder` for building
393 /// a custom client configuration.
394 ///
395 /// # Example
396 /// ```no_run
397 /// # use runpod_sdk::{RunpodClient, Result};
398 /// # use std::time::Duration;
399 /// # async fn example() -> Result<()> {
400 /// let client = RunpodClient::builder()
401 /// .with_api_key("your-api-key")
402 /// .with_timeout(Duration::from_secs(60))
403 /// .build_client()?;
404 /// # Ok(())
405 /// # }
406 /// ```
407 pub fn builder() -> RunpodBuilder {
408 RunpodConfig::builder()
409 }
410
411 /// Creates a new Runpod API client from environment variables.
412 ///
413 /// This is a convenience method that creates a RunpodConfig from environment
414 /// variables and then creates a client from that config.
415 ///
416 /// # Environment Variables
417 ///
418 /// - `RUNPOD_API_KEY` - Your RunPod API key (required)
419 /// - `RUNPOD_BASE_URL` - Base URL for the API (optional, defaults to <https://rest.runpod.io/v1>)
420 /// - `RUNPOD_GRAPHQL_URL` - GraphQL API URL (optional, defaults to <https://api.runpod.io/graphql>, requires `graphql` feature)
421 /// - `RUNPOD_TIMEOUT_SECS` - Request timeout in seconds (optional, defaults to 30)
422 ///
423 /// # Example
424 /// ```no_run
425 /// # use runpod_sdk::{RunpodClient, Result};
426 /// # async fn example() -> Result<()> {
427 /// let client = RunpodClient::from_env()?;
428 /// # Ok(())
429 /// # }
430 /// ```
431 #[cfg_attr(feature = "tracing", tracing::instrument(target = TRACING_TARGET_CLIENT))]
432 pub fn from_env() -> Result<Self> {
433 #[cfg(feature = "tracing")]
434 tracing::debug!(target: TRACING_TARGET_CLIENT, "Creating RunPod client from environment");
435
436 let config = RunpodConfig::from_env()?;
437 Self::new(config)
438 }
439}
440
441impl fmt::Debug for RunpodClient {
442 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443 let mut debug_struct = f.debug_struct("RunpodClient");
444 debug_struct
445 .field("api_key", &self.inner.config.masked_api_key())
446 .field("rest_url", &self.inner.config.rest_url())
447 .field("timeout", &self.inner.config.timeout());
448
449 #[cfg(feature = "graphql")]
450 debug_struct.field("graphql_url", &self.inner.config.graphql_url());
451
452 debug_struct.finish()
453 }
454}