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}