1use serde::de::DeserializeOwned;
4use serde::Serialize;
5use serde_json::Value;
6
7use crate::clients::base::{delete_resource, get_resource, update_resource, ResourceContext};
8use crate::clients::pagination::ListIterator;
9use crate::common::{parse_data_envelope, sign_storage_content, PaginationList, QueryParams};
10use crate::error::ApifyClientResult;
11use crate::http_client::{HttpClient, HttpMethod, HttpRequest};
12use crate::models::Dataset;
13
14#[derive(Debug, Default, Clone)]
19pub struct DatasetListItemsOptions {
20 pub offset: Option<i64>,
22 pub limit: Option<i64>,
24 pub desc: Option<bool>,
26 pub fields: Option<Vec<String>>,
28 pub output_fields: Option<Vec<String>>,
31 pub omit: Option<Vec<String>>,
33 pub skip_empty: Option<bool>,
35 pub skip_hidden: Option<bool>,
37 pub clean: Option<bool>,
39 pub unwind: Option<Vec<String>>,
41 pub flatten: Option<Vec<String>>,
43 pub view: Option<String>,
45 pub simplified: Option<bool>,
47 pub skip_failed_pages: Option<bool>,
49 pub signature: Option<String>,
51}
52
53impl DatasetListItemsOptions {
54 fn apply(&self, params: &mut QueryParams) {
55 params
56 .add_int("offset", self.offset)
57 .add_int("limit", self.limit)
58 .add_bool("desc", self.desc)
59 .add_csv("fields", self.fields.as_deref())
60 .add_csv("outputFields", self.output_fields.as_deref())
61 .add_csv("omit", self.omit.as_deref())
62 .add_bool("skipEmpty", self.skip_empty)
63 .add_bool("skipHidden", self.skip_hidden)
64 .add_bool("clean", self.clean)
65 .add_csv("unwind", self.unwind.as_deref())
66 .add_csv("flatten", self.flatten.as_deref())
67 .add_str("view", self.view.clone())
68 .add_bool("simplified", self.simplified)
69 .add_bool("skipFailedPages", self.skip_failed_pages)
70 .add_str("signature", self.signature.clone());
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum DownloadItemsFormat {
77 Json,
79 Jsonl,
81 Csv,
83 Xlsx,
85 Xml,
87 Rss,
89 Html,
91}
92
93impl DownloadItemsFormat {
94 fn as_str(&self) -> &'static str {
95 match self {
96 DownloadItemsFormat::Json => "json",
97 DownloadItemsFormat::Jsonl => "jsonl",
98 DownloadItemsFormat::Csv => "csv",
99 DownloadItemsFormat::Xlsx => "xlsx",
100 DownloadItemsFormat::Xml => "xml",
101 DownloadItemsFormat::Rss => "rss",
102 DownloadItemsFormat::Html => "html",
103 }
104 }
105}
106
107#[derive(Debug, Default, Clone)]
110pub struct DatasetDownloadOptions {
111 pub items: DatasetListItemsOptions,
113 pub attachment: Option<bool>,
115 pub bom: Option<bool>,
117 pub delimiter: Option<String>,
119 pub skip_header_row: Option<bool>,
121 pub xml_root: Option<String>,
123 pub xml_row: Option<String>,
125 pub feed_title: Option<String>,
127 pub feed_description: Option<String>,
129}
130
131impl DatasetDownloadOptions {
132 fn apply(&self, params: &mut QueryParams) {
133 self.items.apply(params);
134 params
135 .add_bool("attachment", self.attachment)
136 .add_bool("bom", self.bom)
137 .add_str("delimiter", self.delimiter.clone())
138 .add_bool("skipHeaderRow", self.skip_header_row)
139 .add_str("xmlRoot", self.xml_root.clone())
140 .add_str("xmlRow", self.xml_row.clone())
141 .add_str("feedTitle", self.feed_title.clone())
142 .add_str("feedDescription", self.feed_description.clone());
143 }
144}
145
146#[derive(Debug, Clone)]
148pub struct DatasetClient {
149 ctx: ResourceContext,
150}
151
152impl DatasetClient {
153 pub(crate) fn new(http: HttpClient, base_url: &str, resource_path: &str, id: &str) -> Self {
154 Self {
155 ctx: ResourceContext::single(http, base_url, resource_path, id),
156 }
157 }
158
159 pub(crate) fn nested(http: HttpClient, base_url: &str, sub_path: &str) -> Self {
161 Self {
162 ctx: ResourceContext::collection(http, base_url, sub_path),
163 }
164 }
165
166 pub(crate) fn with_public_base(mut self, public_base_url: &str) -> Self {
168 self.ctx = self.ctx.with_public_origin(public_base_url);
169 self
170 }
171
172 pub async fn get(&self) -> ApifyClientResult<Option<Dataset>> {
174 get_resource(&self.ctx, None, &QueryParams::new()).await
175 }
176
177 pub async fn update<T: Serialize>(&self, new_fields: &T) -> ApifyClientResult<Dataset> {
179 update_resource(&self.ctx, None, new_fields).await
180 }
181
182 pub async fn delete(&self) -> ApifyClientResult<()> {
184 delete_resource(&self.ctx, None).await
185 }
186
187 pub async fn list_items<T: DeserializeOwned>(
193 &self,
194 options: DatasetListItemsOptions,
195 ) -> ApifyClientResult<PaginationList<T>> {
196 let mut params = QueryParams::new();
197 options.apply(&mut params);
198 let url = params.apply_to_url(&self.ctx.url(Some("items")));
199 let response = self
200 .ctx
201 .http
202 .call(HttpRequest {
203 method: HttpMethod::Get,
204 url,
205 headers: Default::default(),
206 body: None,
207 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
208 })
209 .await?;
210
211 let items: Vec<T> = serde_json::from_slice(&response.body)?;
212 let count = items.len() as i64;
213 let total = response
217 .header("x-apify-pagination-total")
218 .and_then(|v| v.parse().ok())
219 .unwrap_or(0);
220 let offset = response
221 .header("x-apify-pagination-offset")
222 .and_then(|v| v.parse().ok())
223 .unwrap_or(0);
224 let limit = response
225 .header("x-apify-pagination-limit")
226 .and_then(|v| v.parse().ok())
227 .unwrap_or(count);
228 let scanned = response
232 .header("x-apify-pagination-count")
233 .and_then(|v| v.parse().ok());
234
235 Ok(PaginationList {
236 total,
237 offset,
238 limit,
239 count,
240 desc: options.desc.unwrap_or(false),
241 items,
242 scanned,
243 })
244 }
245
246 pub fn iterate_items<T: DeserializeOwned + Send + 'static>(
265 &self,
266 options: DatasetListItemsOptions,
267 ) -> ListIterator<T> {
268 let client = self.clone();
269 let start = options.offset.unwrap_or(0);
270 let total_limit = options.limit;
271 ListIterator::new(
272 start,
273 total_limit,
274 Box::new(move |offset, page_limit| {
275 let client = client.clone();
276 let mut options = options.clone();
277 options.offset = Some(offset);
278 options.limit = page_limit;
279 Box::pin(async move { client.list_items::<T>(options).await })
280 }),
281 )
282 }
283
284 pub async fn download_items(
291 &self,
292 format: DownloadItemsFormat,
293 options: DatasetDownloadOptions,
294 ) -> ApifyClientResult<Vec<u8>> {
295 let mut params = QueryParams::new();
296 params.add_str("format", Some(format.as_str()));
297 options.apply(&mut params);
298 let url = params.apply_to_url(&self.ctx.url(Some("items")));
299 let response = self
300 .ctx
301 .http
302 .call(HttpRequest {
303 method: HttpMethod::Get,
304 url,
305 headers: Default::default(),
306 body: None,
307 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
308 })
309 .await?;
310 Ok(response.body)
311 }
312
313 pub async fn push_items<T: Serialize>(&self, items: &T) -> ApifyClientResult<()> {
317 let body = serde_json::to_vec(items)?;
318 let url = self.ctx.url(Some("items"));
319 let mut headers = std::collections::HashMap::new();
320 headers.insert(
321 "Content-Type".to_string(),
322 "application/json; charset=utf-8".to_string(),
323 );
324 self.ctx
325 .http
326 .call(HttpRequest {
327 method: HttpMethod::Post,
328 url,
329 headers,
330 body: Some(body),
331 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
332 })
333 .await?;
334 Ok(())
335 }
336
337 pub async fn create_items_public_url(
346 &self,
347 options: DatasetListItemsOptions,
348 expires_in_secs: Option<i64>,
349 ) -> ApifyClientResult<String> {
350 self.create_items_public_url_with_format(options, expires_in_secs, None)
351 .await
352 }
353
354 pub async fn create_items_public_url_with_format(
359 &self,
360 options: DatasetListItemsOptions,
361 expires_in_secs: Option<i64>,
362 format: Option<DownloadItemsFormat>,
363 ) -> ApifyClientResult<String> {
364 let mut params = QueryParams::new();
365 options.apply(&mut params);
366 params.add_str("format", format.map(|f| f.as_str().to_string()));
367
368 if let Some(dataset) = self.get().await? {
369 if let Some(secret) = dataset
370 .extra
371 .get("urlSigningSecretKey")
372 .and_then(|v| v.as_str())
373 {
374 let signature = sign_storage_content(secret, &dataset.id, expires_in_secs);
375 params.add_str("signature", Some(signature));
376 }
377 }
378 Ok(params.apply_to_url(&self.ctx.public_url(Some("items"))))
379 }
380
381 pub async fn get_statistics(&self) -> ApifyClientResult<Option<Value>> {
383 let result: ApifyClientResult<Value> = async {
384 let response = self
385 .ctx
386 .http
387 .call(HttpRequest {
388 method: HttpMethod::Get,
389 url: self.ctx.url(Some("statistics")),
390 headers: Default::default(),
391 body: None,
392 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
393 })
394 .await?;
395 parse_data_envelope(&response.body)
396 }
397 .await;
398 crate::common::catch_not_found(result)
399 }
400}