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