Skip to main content

apify_client/
client.rs

1//! The top-level [`ApifyClient`] and its [`ApifyClientBuilder`].
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use crate::clients::actor::ActorClient;
7use crate::clients::actor_collection::ActorCollectionClient;
8use crate::clients::build::BuildClient;
9use crate::clients::build_collection::BuildCollectionClient;
10use crate::clients::dataset::DatasetClient;
11use crate::clients::dataset_collection::DatasetCollectionClient;
12use crate::clients::key_value_store::KeyValueStoreClient;
13use crate::clients::key_value_store_collection::KeyValueStoreCollectionClient;
14use crate::clients::log::LogClient;
15use crate::clients::request_queue::RequestQueueClient;
16use crate::clients::request_queue_collection::RequestQueueCollectionClient;
17use crate::clients::run::RunClient;
18use crate::clients::run_collection::RunCollectionClient;
19use crate::clients::schedule::ScheduleClient;
20use crate::clients::schedule_collection::ScheduleCollectionClient;
21use crate::clients::store_collection::StoreCollectionClient;
22use crate::clients::task::TaskClient;
23use crate::clients::task_collection::TaskCollectionClient;
24use crate::clients::user::UserClient;
25use crate::clients::webhook::WebhookClient;
26use crate::clients::webhook_collection::WebhookCollectionClient;
27use crate::clients::webhook_dispatch::WebhookDispatchClient;
28use crate::clients::webhook_dispatch_collection::WebhookDispatchCollectionClient;
29use crate::common::build_user_agent;
30use crate::http_client::{HttpBackend, HttpClient, ReqwestBackend, RetryConfig};
31
32/// Default base URL of the Apify API (without the `/v2` suffix).
33const DEFAULT_BASE_URL: &str = "https://api.apify.com";
34/// Default maximum number of retries, matching the reference clients.
35const DEFAULT_MAX_RETRIES: u32 = 8;
36/// Default minimum delay between retries.
37const DEFAULT_MIN_DELAY_BETWEEN_RETRIES: Duration = Duration::from_millis(500);
38/// Default overall per-request timeout (6 minutes), matching the reference clients.
39const DEFAULT_TIMEOUT: Duration = Duration::from_secs(360);
40/// Placeholder used to address the current user (`/users/me`).
41pub(crate) const ME_USER_PLACEHOLDER: &str = "me";
42
43/// Builder for [`ApifyClient`].
44///
45/// # Example
46/// ```no_run
47/// use apify_client::ApifyClient;
48///
49/// let client = ApifyClient::builder()
50///     .token("my-api-token")
51///     .max_retries(5)
52///     .build();
53/// ```
54pub struct ApifyClientBuilder {
55    token: Option<String>,
56    base_url: String,
57    public_base_url: Option<String>,
58    max_retries: u32,
59    min_delay_between_retries: Duration,
60    timeout: Duration,
61    user_agent_suffix: Option<String>,
62    http_backend: Option<Arc<dyn HttpBackend>>,
63}
64
65impl Default for ApifyClientBuilder {
66    fn default() -> Self {
67        Self {
68            token: None,
69            base_url: DEFAULT_BASE_URL.to_string(),
70            public_base_url: None,
71            max_retries: DEFAULT_MAX_RETRIES,
72            min_delay_between_retries: DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
73            timeout: DEFAULT_TIMEOUT,
74            user_agent_suffix: None,
75            http_backend: None,
76        }
77    }
78}
79
80impl ApifyClientBuilder {
81    /// Sets the API token used for authentication (sent as a `Bearer` token).
82    pub fn token(mut self, token: impl Into<String>) -> Self {
83        self.token = Some(token.into());
84        self
85    }
86
87    /// Overrides the base URL of the API. The `/v2` suffix is appended automatically.
88    ///
89    /// Defaults to `https://api.apify.com`.
90    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
91        self.base_url = base_url.into();
92        self
93    }
94
95    /// Overrides the base URL used when building public, shareable resource URLs (e.g. a
96    /// signed dataset-items URL). Defaults to the API base URL. The `/v2` suffix is appended
97    /// automatically.
98    pub fn public_base_url(mut self, public_base_url: impl Into<String>) -> Self {
99        self.public_base_url = Some(public_base_url.into());
100        self
101    }
102
103    /// Sets the maximum number of retries for failed requests (default `8`).
104    pub fn max_retries(mut self, max_retries: u32) -> Self {
105        self.max_retries = max_retries;
106        self
107    }
108
109    /// Sets the minimum delay between retries (default `500ms`).
110    pub fn min_delay_between_retries(mut self, delay: Duration) -> Self {
111        self.min_delay_between_retries = delay;
112        self
113    }
114
115    /// Sets the overall per-request timeout (default `360s`).
116    pub fn timeout(mut self, timeout: Duration) -> Self {
117        self.timeout = timeout;
118        self
119    }
120
121    /// Appends a custom suffix to the `User-Agent` header.
122    pub fn user_agent_suffix(mut self, suffix: impl Into<String>) -> Self {
123        self.user_agent_suffix = Some(suffix.into());
124        self
125    }
126
127    /// Replaces the default [`reqwest`]-based HTTP backend with a custom implementation.
128    ///
129    /// This is the seam that makes the transport a replaceable component.
130    pub fn http_backend(mut self, backend: Arc<dyn HttpBackend>) -> Self {
131        self.http_backend = Some(backend);
132        self
133    }
134
135    /// Builds the configured [`ApifyClient`].
136    pub fn build(self) -> ApifyClient {
137        let backend = self
138            .http_backend
139            .unwrap_or_else(|| Arc::new(ReqwestBackend::new()));
140
141        let user_agent = build_user_agent(self.user_agent_suffix.as_deref());
142
143        let retry = RetryConfig {
144            max_retries: self.max_retries,
145            min_delay_between_retries: self.min_delay_between_retries,
146            timeout: self.timeout,
147        };
148
149        let http = HttpClient::new(backend, self.token, user_agent, retry);
150
151        let trimmed = self.base_url.trim_end_matches('/');
152        let base_url = format!("{trimmed}/v2");
153
154        let public_trimmed = self
155            .public_base_url
156            .as_deref()
157            .unwrap_or(&self.base_url)
158            .trim_end_matches('/');
159        let public_base_url = format!("{public_trimmed}/v2");
160
161        ApifyClient {
162            http,
163            base_url,
164            public_base_url,
165        }
166    }
167}
168
169/// The entry point for interacting with the Apify API.
170///
171/// Construct it with [`ApifyClient::builder`] (or [`ApifyClient::new`] for a quick
172/// token-only setup), then obtain resource clients via the accessor methods, e.g.
173/// [`ApifyClient::actor`], [`ApifyClient::dataset`], [`ApifyClient::run`].
174///
175/// The client is cheap to clone — all internal state is reference-counted.
176#[derive(Debug, Clone)]
177pub struct ApifyClient {
178    http: HttpClient,
179    base_url: String,
180    public_base_url: String,
181}
182
183impl ApifyClient {
184    /// Returns a [`ApifyClientBuilder`] for configuring a client.
185    pub fn builder() -> ApifyClientBuilder {
186        ApifyClientBuilder::default()
187    }
188
189    /// Creates a client authenticated with the given API token and default settings.
190    pub fn new(token: impl Into<String>) -> Self {
191        ApifyClientBuilder::default().token(token).build()
192    }
193
194    pub(crate) fn http(&self) -> HttpClient {
195        self.http.clone()
196    }
197
198    /// The `User-Agent` header value this client sends.
199    pub fn user_agent(&self) -> &str {
200        self.http.user_agent()
201    }
202
203    /// The fully-qualified API base URL this client targets (including the `/v2` suffix),
204    /// e.g. `https://api.apify.com/v2`. Reflects any `base_url` override.
205    pub fn api_base_url(&self) -> &str {
206        &self.base_url
207    }
208
209    // ----- Actor accessors -------------------------------------------------
210
211    /// Returns a client for the Actor collection (list & create Actors).
212    pub fn actors(&self) -> ActorCollectionClient {
213        ActorCollectionClient::new(self.http(), &self.base_url)
214    }
215
216    /// Returns a client for a specific Actor, addressed by ID or `username~name`.
217    pub fn actor(&self, id: impl Into<String>) -> ActorClient {
218        ActorClient::new(self.clone(), self.http(), &self.base_url, &id.into())
219    }
220
221    // ----- Build accessors -------------------------------------------------
222
223    /// Returns a client for the Actor build collection (list builds).
224    pub fn builds(&self) -> BuildCollectionClient {
225        BuildCollectionClient::new(self.http(), &self.base_url)
226    }
227
228    /// Returns a client for a specific Actor build.
229    pub fn build(&self, id: impl Into<String>) -> BuildClient {
230        BuildClient::new(self.http(), &self.base_url, &id.into())
231    }
232
233    // ----- Run accessors ---------------------------------------------------
234
235    /// Returns a client for the Actor run collection (list runs).
236    pub fn runs(&self) -> RunCollectionClient {
237        RunCollectionClient::new(self.http(), &self.base_url, "actor-runs")
238    }
239
240    /// Returns a client for a specific Actor run.
241    pub fn run(&self, id: impl Into<String>) -> RunClient {
242        RunClient::new(
243            self.clone(),
244            self.http(),
245            &self.base_url,
246            "actor-runs",
247            &id.into(),
248        )
249    }
250
251    // ----- Dataset accessors ----------------------------------------------
252
253    /// Returns a client for the dataset collection (list & get-or-create datasets).
254    pub fn datasets(&self) -> DatasetCollectionClient {
255        DatasetCollectionClient::new(self.http(), &self.base_url)
256    }
257
258    /// Returns a client for a specific dataset, addressed by ID or name.
259    pub fn dataset(&self, id: impl Into<String>) -> DatasetClient {
260        DatasetClient::new(self.http(), &self.base_url, "datasets", &id.into())
261            .with_public_base(&self.public_base_url)
262    }
263
264    // ----- Key-value store accessors --------------------------------------
265
266    /// Returns a client for the key-value store collection.
267    pub fn key_value_stores(&self) -> KeyValueStoreCollectionClient {
268        KeyValueStoreCollectionClient::new(self.http(), &self.base_url)
269    }
270
271    /// Returns a client for a specific key-value store, addressed by ID or name.
272    pub fn key_value_store(&self, id: impl Into<String>) -> KeyValueStoreClient {
273        KeyValueStoreClient::new(self.http(), &self.base_url, "key-value-stores", &id.into())
274            .with_public_base(&self.public_base_url)
275    }
276
277    // ----- Request queue accessors ----------------------------------------
278
279    /// Returns a client for the request queue collection.
280    pub fn request_queues(&self) -> RequestQueueCollectionClient {
281        RequestQueueCollectionClient::new(self.http(), &self.base_url)
282    }
283
284    /// Returns a client for a specific request queue, addressed by ID or name.
285    pub fn request_queue(&self, id: impl Into<String>) -> RequestQueueClient {
286        RequestQueueClient::new(self.http(), &self.base_url, "request-queues", &id.into())
287    }
288
289    // ----- Task accessors --------------------------------------------------
290
291    /// Returns a client for the Actor task collection (list & create tasks).
292    pub fn tasks(&self) -> TaskCollectionClient {
293        TaskCollectionClient::new(self.http(), &self.base_url)
294    }
295
296    /// Returns a client for a specific Actor task.
297    pub fn task(&self, id: impl Into<String>) -> TaskClient {
298        TaskClient::new(self.clone(), self.http(), &self.base_url, &id.into())
299    }
300
301    // ----- Schedule accessors ---------------------------------------------
302
303    /// Returns a client for the schedule collection (list & create schedules).
304    pub fn schedules(&self) -> ScheduleCollectionClient {
305        ScheduleCollectionClient::new(self.http(), &self.base_url)
306    }
307
308    /// Returns a client for a specific schedule.
309    pub fn schedule(&self, id: impl Into<String>) -> ScheduleClient {
310        ScheduleClient::new(self.http(), &self.base_url, &id.into())
311    }
312
313    // ----- Webhook accessors ----------------------------------------------
314
315    /// Returns a client for the webhook collection (list & create webhooks).
316    pub fn webhooks(&self) -> WebhookCollectionClient {
317        WebhookCollectionClient::new(self.http(), &self.base_url)
318    }
319
320    /// Returns a client for a specific webhook.
321    pub fn webhook(&self, id: impl Into<String>) -> WebhookClient {
322        WebhookClient::new(self.http(), &self.base_url, &id.into())
323    }
324
325    /// Returns a client for the webhook dispatch collection.
326    pub fn webhook_dispatches(&self) -> WebhookDispatchCollectionClient {
327        WebhookDispatchCollectionClient::new(self.http(), &self.base_url)
328    }
329
330    /// Returns a client for a specific webhook dispatch.
331    pub fn webhook_dispatch(&self, id: impl Into<String>) -> WebhookDispatchClient {
332        WebhookDispatchClient::new(self.http(), &self.base_url, &id.into())
333    }
334
335    // ----- Misc accessors --------------------------------------------------
336
337    /// Returns a client for browsing the Apify Store.
338    pub fn store(&self) -> StoreCollectionClient {
339        StoreCollectionClient::new(self.http(), &self.base_url)
340    }
341
342    /// Returns a client for accessing a build's or run's log.
343    pub fn log(&self, build_or_run_id: impl Into<String>) -> LogClient {
344        LogClient::new(self.http(), &self.base_url, "logs", &build_or_run_id.into())
345    }
346
347    /// Returns a client for the current user (`/users/me`).
348    pub fn me(&self) -> UserClient {
349        UserClient::new(self.http(), &self.base_url, ME_USER_PLACEHOLDER)
350    }
351
352    /// Returns a client for a specific user by ID or username.
353    pub fn user(&self, id: impl Into<String>) -> UserClient {
354        UserClient::new(self.http(), &self.base_url, &id.into())
355    }
356
357    /// Sets the status message of the current Actor run.
358    ///
359    /// This convenience method updates the run identified by the `ACTOR_RUN_ID` environment
360    /// variable, so it only works when called from inside an Actor run. If
361    /// `is_terminal` is `true`, the message becomes final and won't be overwritten.
362    ///
363    /// Returns [`ApifyClientError::InvalidArgument`](crate::ApifyClientError::InvalidArgument)
364    /// if `ACTOR_RUN_ID` is not set.
365    pub async fn set_status_message(
366        &self,
367        message: &str,
368        is_terminal: bool,
369    ) -> crate::error::ApifyClientResult<crate::models::ActorRun> {
370        let run_id = std::env::var("ACTOR_RUN_ID").map_err(|_| {
371            crate::error::ApifyClientError::InvalidArgument(
372                "ACTOR_RUN_ID environment variable is not set".to_string(),
373            )
374        })?;
375        let body = serde_json::json!({
376            "statusMessage": message,
377            "isStatusMessageTerminal": is_terminal,
378        });
379        self.run(run_id).update(&body).await
380    }
381}