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}