Skip to main content

apify_rs/
models.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5// ---------------------------------------------------------------------------
6// Generic response wrappers
7// ---------------------------------------------------------------------------
8
9/// Apify wraps almost every successful response in `{ "data": { ... } }`.
10///
11/// `HttpClient` automatically unwraps this for you; you rarely need to
12/// use this type directly.
13#[derive(Debug, Clone, Deserialize, Serialize)]
14pub struct DataResponse<T> {
15    pub data: T,
16}
17
18/// Apify wraps paginated list responses in `{ "data": { total, offset, limit, count, desc, items } }`.
19///
20/// `HttpClient::get_list` returns the inner [`ListData`] directly.
21#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct ListResponse<T> {
23    pub data: ListData<T>,
24}
25
26/// Pagination metadata plus the actual collection items.
27#[derive(Debug, Clone, Deserialize, Serialize)]
28pub struct ListData<T> {
29    /// Total number of items available across all pages.
30    pub total: u64,
31    /// How many items were skipped at the start of the list.
32    pub offset: u64,
33    /// Maximum number of items the server agreed to return in this page.
34    pub limit: u64,
35    /// Actual number of items in this response.
36    pub count: u64,
37    /// `true` when the list is sorted newest-first.
38    pub desc: bool,
39    /// The page of results.
40    pub items: Vec<T>,
41}
42
43// ---------------------------------------------------------------------------
44// Actor
45// ---------------------------------------------------------------------------
46
47/// A serverless program published on the Apify platform.
48///
49/// Actors are the atomic unit of work on Apify: they accept a JSON input,
50/// perform a job (scrape a website, process a file, etc.), and write results
51/// to a Dataset or Key-Value Store.  Each Actor has one or more versions
52/// (e.g. `latest`, `beta`) and configurable run options (memory, timeout).
53#[derive(Debug, Clone, Deserialize, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct Actor {
56    pub id: String,
57    pub user_id: String,
58    pub name: String,
59    pub username: String,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub description: Option<String>,
62    pub is_public: bool,
63    pub created_at: DateTime<Utc>,
64    pub modified_at: DateTime<Utc>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub stats: Option<ActorStats>,
67    pub versions: Vec<ActorVersion>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub default_run_options: Option<RunOptions>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub title: Option<String>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub is_deprecated: Option<bool>,
74}
75
76/// Usage statistics for an Actor (total runs, last run time, …).
77#[derive(Debug, Clone, Deserialize, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct ActorStats {
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub total_runs: Option<u64>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub last_run_started_at: Option<DateTime<Utc>>,
84}
85
86/// A single version entry of an Actor (e.g. `latest` tag or a numbered build).
87#[derive(Debug, Clone, Deserialize, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct ActorVersion {
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub version_number: Option<String>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub source_type: Option<String>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub build_tag: Option<String>,
96}
97
98// ---------------------------------------------------------------------------
99// Task
100// ---------------------------------------------------------------------------
101
102/// A saved configuration for running an Actor.
103///
104/// Tasks let you freeze an Actor ID, input JSON, and run options so you
105/// can launch the same job repeatedly without resending the full input.
106/// They are the preferred way to schedule or automate recurring work.
107#[derive(Debug, Clone, Deserialize, Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct Task {
110    pub id: String,
111    pub user_id: String,
112    pub act_id: String,
113    pub name: String,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub username: Option<String>,
116    pub created_at: DateTime<Utc>,
117    pub modified_at: DateTime<Utc>,
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub removed_at: Option<DateTime<Utc>>,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub stats: Option<TaskStats>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub options: Option<TaskOptions>,
124    /// The JSON input that will be passed to the Actor on every run.
125    /// Use a typed struct for a specific Actor instead of `serde_json::Value`
126    /// when you know the schema.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub input: Option<serde_json::Value>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub title: Option<String>,
131}
132
133/// Quick statistics for a Task.
134#[derive(Debug, Clone, Deserialize, Serialize)]
135#[serde(rename_all = "camelCase")]
136pub struct TaskStats {
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub total_runs: Option<u64>,
139}
140
141/// Run-time options that can be attached to a Task or overridden per-Run.
142///
143/// These control how the Actor container is provisioned on Apify's infrastructure.
144#[derive(Debug, Clone, Default, Deserialize, Serialize)]
145#[serde(rename_all = "camelCase")]
146pub struct TaskOptions {
147    /// Actor build tag or number to run (e.g. `latest`).
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub build: Option<String>,
150    /// Maximum run time in seconds before Apify forcibly terminates the container.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub timeout_secs: Option<u64>,
153    /// Memory allocated to the container, in megabytes.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub memory_mbytes: Option<u64>,
156    /// Stop the Actor after this many result items (pay-per-result Actors).
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub max_items: Option<u64>,
159    /// Maximum dollar amount you are willing to spend on this run.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub max_total_charge_usd: Option<f64>,
162    /// Automatically restart the Actor on a non-zero exit code.
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub restart_on_error: Option<bool>,
165}
166
167// ---------------------------------------------------------------------------
168// Run
169// ---------------------------------------------------------------------------
170
171/// A single execution of an Actor or Task.
172///
173/// When you start a Run, Apify provisions a Docker container, streams logs,
174/// and eventually writes results to a **Dataset** (tabular data) and/or a
175/// **Key-Value Store** (files, JSON snapshots).  The IDs of those default
176/// stores are available on this struct as `default_dataset_id` and
177/// `default_key_value_store_id`.
178#[derive(Debug, Clone, Deserialize, Serialize)]
179#[serde(rename_all = "camelCase")]
180pub struct Run {
181    pub id: String,
182    pub act_id: String,
183    pub user_id: String,
184    /// If this run was started from a Task, the Task's ID.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub actor_task_id: Option<String>,
187    pub started_at: DateTime<Utc>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub finished_at: Option<DateTime<Utc>>,
190    pub status: ActorJobStatus,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub status_message: Option<String>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub is_status_message_terminal: Option<bool>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub meta: Option<RunMeta>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub stats: Option<RunStats>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub options: Option<RunOptions>,
201    pub build_id: String,
202    /// Docker container exit code (`0` = success, `1` = error, …).
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub exit_code: Option<i32>,
205    /// Key-Value Store allocated specifically for this run.
206    pub default_key_value_store_id: String,
207    /// Dataset allocated specifically for this run.
208    pub default_dataset_id: String,
209    /// Request Queue allocated specifically for this run.
210    pub default_request_queue_id: String,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub storage_ids: Option<StorageIds>,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub build_number: Option<String>,
215    /// URL of the live container (useful for debugging or web-server Actors).
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub container_url: Option<String>,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub is_container_server_ready: Option<bool>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub git_branch_name: Option<String>,
222    /// Estimated cost of this run in USD.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub usage_total_usd: Option<f64>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub charged_event_counts: Option<HashMap<String, u64>>,
227}
228
229/// Metadata about how a Run was triggered (origin, client IP, user-agent, …).
230#[derive(Debug, Clone, Deserialize, Serialize)]
231#[serde(rename_all = "camelCase")]
232pub struct RunMeta {
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub origin: Option<String>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub client_ip: Option<String>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub user_agent: Option<String>,
239}
240
241/// Resource-usage metrics collected during a Run.
242#[derive(Debug, Clone, Deserialize, Serialize)]
243#[serde(rename_all = "camelCase")]
244pub struct RunStats {
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub input_body_len: Option<u64>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub restart_count: Option<u64>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub resurrect_count: Option<u64>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub mem_avg_mbytes: Option<f64>,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub mem_max_mbytes: Option<f64>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub cpu_avg_usage: Option<f64>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub cpu_max_usage: Option<f64>,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub duration_millis: Option<u64>,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub run_time_secs: Option<f64>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub metamorph: Option<u64>,
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub compute_units: Option<f64>,
267}
268
269/// Run-time overrides that apply only to a single Run.
270///
271/// A subset of [`TaskOptions`] — no `max_total_charge_usd` or `restart_on_error`
272/// because those are Task-level settings.
273#[derive(Debug, Clone, Default, Deserialize, Serialize)]
274#[serde(rename_all = "camelCase")]
275pub struct RunOptions {
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub build: Option<String>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub timeout_secs: Option<u64>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub memory_mbytes: Option<u64>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub max_items: Option<u64>,
284}
285
286/// Map of named storage IDs attached to a Run beyond the defaults.
287#[derive(Debug, Clone, Deserialize, Serialize)]
288#[serde(rename_all = "camelCase")]
289pub struct StorageIds {
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub datasets: Option<HashMap<String, String>>,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub key_value_stores: Option<HashMap<String, String>>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub request_queues: Option<HashMap<String, String>>,
296}
297
298/// Lifecycle state of an Actor Run (or Build).
299///
300/// Terminal states: [`Succeeded`](ActorJobStatus::Succeeded),
301/// [`Failed`](ActorJobStatus::Failed),
302/// [`TimedOut`](ActorJobStatus::TimedOut),
303/// [`Aborted`](ActorJobStatus::Aborted).
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
305#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
306pub enum ActorJobStatus {
307    /// Queued, waiting for a container slot.
308    Ready,
309    /// Actively executing.
310    Running,
311    /// Finished successfully; output is ready.
312    Succeeded,
313    /// Finished with a non-zero exit code or unhandled exception.
314    Failed,
315    /// The `timeout_secs` threshold was reached; Apify is terminating the container.
316    TimingOut,
317    /// The container was killed because it exceeded its time limit.
318    TimedOut,
319    /// A user or API call requested cancellation; the signal is in flight.
320    Aborting,
321    /// The run was cancelled before natural completion.
322    Aborted,
323}
324
325impl ActorJobStatus {
326    /// Returns `true` for states where the container is no longer running.
327    pub fn is_terminal(self) -> bool {
328        matches!(
329            self,
330            ActorJobStatus::Succeeded
331                | ActorJobStatus::Failed
332                | ActorJobStatus::TimedOut
333                | ActorJobStatus::Aborted
334        )
335    }
336
337    /// Returns `true` only for [`ActorJobStatus::Succeeded`].
338    pub fn is_success(self) -> bool {
339        self == ActorJobStatus::Succeeded
340    }
341}
342
343// ---------------------------------------------------------------------------
344// Lightweight list items
345// ---------------------------------------------------------------------------
346
347/// Compact Actor representation returned by list endpoints.
348#[derive(Debug, Clone, Deserialize, Serialize)]
349#[serde(rename_all = "camelCase")]
350pub struct ActorShort {
351    pub id: String,
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub name: Option<String>,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub username: Option<String>,
356    pub created_at: DateTime<Utc>,
357    pub modified_at: DateTime<Utc>,
358}
359
360/// Compact Task representation returned by list endpoints.
361#[derive(Debug, Clone, Deserialize, Serialize)]
362#[serde(rename_all = "camelCase")]
363pub struct TaskShort {
364    pub id: String,
365    pub act_id: String,
366    pub name: String,
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub username: Option<String>,
369    pub created_at: DateTime<Utc>,
370    pub modified_at: DateTime<Utc>,
371}
372
373/// Compact Run representation returned by list endpoints.
374#[derive(Debug, Clone, Deserialize, Serialize)]
375#[serde(rename_all = "camelCase")]
376pub struct RunShort {
377    pub id: String,
378    pub act_id: String,
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub actor_task_id: Option<String>,
381    pub started_at: DateTime<Utc>,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub finished_at: Option<DateTime<Utc>>,
384    pub status: ActorJobStatus,
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub build_id: Option<String>,
387}
388
389// ---------------------------------------------------------------------------
390// Request helpers
391// ---------------------------------------------------------------------------
392
393/// Query parameters for paginated `list` operations.
394#[derive(Debug, Clone, Default, Serialize)]
395#[serde(rename_all = "camelCase")]
396pub struct ListParams {
397    /// Skip this many items from the start of the list.
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub offset: Option<u64>,
400    /// Maximum items to return (capped by the server).
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub limit: Option<u64>,
403    /// Sort newest-first when `true`.
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub desc: Option<bool>,
406}
407
408/// Query parameters for starting a Run.
409///
410/// Sent as the URL query string on `POST …/runs` or `GET …/run-sync`.
411#[derive(Debug, Clone, Default, Serialize)]
412#[serde(rename_all = "camelCase")]
413pub struct RunParams {
414    /// Build tag or number (e.g. `latest`).
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub build: Option<String>,
417    /// Override the Actor's default timeout (seconds).
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub timeout_secs: Option<u64>,
420    /// Override the Actor's default memory (MB).
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub memory_mbytes: Option<u64>,
423    /// Stop after this many items (pay-per-result Actors).
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub max_items: Option<u64>,
426    /// Cap the dollar cost of this run.
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub max_total_charge_usd: Option<f64>,
429    /// For synchronous endpoints: how many seconds to wait for the run to finish
430    /// before the HTTP response returns.
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub wait_for_finish: Option<u64>,
433}
434
435/// Body of the `POST /actor-tasks` request.
436#[derive(Debug, Clone, Serialize)]
437#[serde(rename_all = "camelCase")]
438pub struct CreateTaskRequest<T: Serialize> {
439    /// Full Actor ID or `username~actor-name`.
440    pub act_id: String,
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub name: Option<String>,
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub options: Option<TaskOptions>,
445    /// Typed input that matches the Actor's JSON schema.
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub input: Option<T>,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub title: Option<String>,
450}
451
452/// Body of the `PUT /actor-tasks/{id}` request.
453///
454/// All fields are optional; omitted fields leave the existing Task value
455/// unchanged.
456#[derive(Debug, Clone, Default, Serialize)]
457#[serde(rename_all = "camelCase")]
458pub struct UpdateTaskRequest<T: Serialize> {
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub name: Option<String>,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub options: Option<TaskOptions>,
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub input: Option<T>,
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub title: Option<String>,
467}
468
469/// Alias for a single row inside a Dataset.
470///
471/// Datasets are Apify's structured tabular storage.  Each Actor run gets a
472/// *default* dataset whose ID is exposed on [`Run::default_dataset_id`].
473pub type DatasetItem = serde_json::Value;
474
475/// Example typed input for an Instagram-scraping Actor.
476///
477/// **This is illustrative only.**  Every Actor on Apify defines its own
478/// input JSON schema.  You should check the Actor's README in the Apify
479/// Console and create a matching Rust struct for the Actor you actually use.
480///
481/// The pattern is always the same: a struct that derives `Serialize`, where
482/// each field maps to a key the Actor expects in its input object.
483#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct InstagramActorInput {
485    /// Instagram handles to scrape (without the leading `@`).
486    ///
487    /// Example: `vec!["apify".into(), "rustlang".into()]`
488    pub usernames: Vec<String>,
489
490    /// Cap the number of results (posts, comments, …) per username.
491    /// `None` lets the Actor use its own default.
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub results_limit: Option<u64>,
494
495    /// Proxy settings — strongly recommended for scraping to avoid IP blocks.
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub proxy_configuration: Option<ProxyConfiguration>,
498}
499
500/// Proxy settings for an Actor run.
501///
502/// Apify runs its own proxy infrastructure (datacenter, residential, …).
503/// Most scraping Actors require proxies to reach target sites reliably.
504///
505/// # Example
506/// ```ignore
507/// let proxy = ProxyConfiguration {
508///     use_apify_proxy: Some(true),
509///     apify_proxy_groups: Some(vec!["RESIDENTIAL".to_string()]),
510///     apify_proxy_country: Some("US".to_string()),
511/// };
512/// ```
513#[derive(Debug, Clone, Serialize, Deserialize)]
514pub struct ProxyConfiguration {
515    /// Enable Apify's managed proxy service.
516    #[serde(skip_serializing_if = "Option::is_none")]
517    pub use_apify_proxy: Option<bool>,
518
519    /// Proxy groups to pull IPs from.
520    ///
521    /// Common values: `["RESIDENTIAL"]`, `["SHADER"]`, `["BUYPROXIES94952"]`.
522    /// Verify which groups your subscription includes.
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub apify_proxy_groups: Option<Vec<String>>,
525
526    /// ISO country code for geo-targeted proxies (e.g. `"US"`, `"DE"`).
527    /// Only honoured by groups that support country selection (e.g. RESIDENTIAL).
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub apify_proxy_country: Option<String>,
530}