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}