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
229 Ok(PaginationList {
230 total,
231 offset,
232 limit,
233 count,
234 desc: options.desc.unwrap_or(false),
235 items,
236 })
237 }
238
239 pub fn iterate_items<T: DeserializeOwned + Send + 'static>(
263 &self,
264 options: DatasetListItemsOptions,
265 ) -> ListIterator<T> {
266 let client = self.clone();
267 let start = options.offset.unwrap_or(0);
268 let total_limit = options.limit;
269 ListIterator::new(
270 start,
271 total_limit,
272 Box::new(move |offset, page_limit| {
273 let client = client.clone();
274 let mut options = options.clone();
275 options.offset = Some(offset);
276 options.limit = page_limit;
277 Box::pin(async move { client.list_items::<T>(options).await })
278 }),
279 )
280 }
281
282 pub async fn download_items(
289 &self,
290 format: DownloadItemsFormat,
291 options: DatasetDownloadOptions,
292 ) -> ApifyClientResult<Vec<u8>> {
293 let mut params = QueryParams::new();
294 params.add_str("format", Some(format.as_str()));
295 options.apply(&mut params);
296 let url = params.apply_to_url(&self.ctx.url(Some("items")));
297 let response = self
298 .ctx
299 .http
300 .call(HttpRequest {
301 method: HttpMethod::Get,
302 url,
303 headers: Default::default(),
304 body: None,
305 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
306 })
307 .await?;
308 Ok(response.body)
309 }
310
311 pub async fn push_items<T: Serialize>(&self, items: &T) -> ApifyClientResult<()> {
315 let body = serde_json::to_vec(items)?;
316 let url = self.ctx.url(Some("items"));
317 let mut headers = std::collections::HashMap::new();
318 headers.insert(
319 "Content-Type".to_string(),
320 "application/json; charset=utf-8".to_string(),
321 );
322 self.ctx
323 .http
324 .call(HttpRequest {
325 method: HttpMethod::Post,
326 url,
327 headers,
328 body: Some(body),
329 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
330 })
331 .await?;
332 Ok(())
333 }
334
335 pub async fn create_items_public_url(
343 &self,
344 options: DatasetListItemsOptions,
345 expires_in_secs: Option<i64>,
346 ) -> ApifyClientResult<String> {
347 let mut params = QueryParams::new();
348 options.apply(&mut params);
349
350 if let Some(dataset) = self.get().await? {
351 if let Some(secret) = dataset
352 .extra
353 .get("urlSigningSecretKey")
354 .and_then(|v| v.as_str())
355 {
356 let signature = sign_storage_content(secret, &dataset.id, expires_in_secs);
357 params.add_str("signature", Some(signature));
358 }
359 }
360 Ok(params.apply_to_url(&self.ctx.public_url(Some("items"))))
361 }
362
363 pub async fn get_statistics(&self) -> ApifyClientResult<Option<Value>> {
365 let result: ApifyClientResult<Value> = async {
366 let response = self
367 .ctx
368 .http
369 .call(HttpRequest {
370 method: HttpMethod::Get,
371 url: self.ctx.url(Some("statistics")),
372 headers: Default::default(),
373 body: None,
374 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
375 })
376 .await?;
377 parse_data_envelope(&response.body)
378 }
379 .await;
380 crate::common::catch_not_found(result)
381 }
382}