scrapely-client-python 3.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of scrapely-client-python might be problematic. Click here for more details.

Files changed (65) hide show
  1. scrapely_client/__init__.py +30 -0
  2. scrapely_client/_charging.py +186 -0
  3. scrapely_client/_client_registry.py +136 -0
  4. scrapely_client/_consts.py +96 -0
  5. scrapely_client/_docs.py +37 -0
  6. scrapely_client/_literals.py +539 -0
  7. scrapely_client/_logging.py +180 -0
  8. scrapely_client/_models.py +4172 -0
  9. scrapely_client/_pagination.py +220 -0
  10. scrapely_client/_resource_clients/__init__.py +84 -0
  11. scrapely_client/_resource_clients/_resource_client.py +567 -0
  12. scrapely_client/_resource_clients/actor.py +1042 -0
  13. scrapely_client/_resource_clients/actor_collection.py +377 -0
  14. scrapely_client/_resource_clients/actor_env_var.py +154 -0
  15. scrapely_client/_resource_clients/actor_env_var_collection.py +165 -0
  16. scrapely_client/_resource_clients/actor_version.py +254 -0
  17. scrapely_client/_resource_clients/actor_version_collection.py +232 -0
  18. scrapely_client/_resource_clients/build.py +244 -0
  19. scrapely_client/_resource_clients/build_collection.py +164 -0
  20. scrapely_client/_resource_clients/dataset.py +1151 -0
  21. scrapely_client/_resource_clients/dataset_collection.py +244 -0
  22. scrapely_client/_resource_clients/key_value_store.py +906 -0
  23. scrapely_client/_resource_clients/key_value_store_collection.py +243 -0
  24. scrapely_client/_resource_clients/log.py +225 -0
  25. scrapely_client/_resource_clients/request_queue.py +1167 -0
  26. scrapely_client/_resource_clients/request_queue_collection.py +240 -0
  27. scrapely_client/_resource_clients/run.py +875 -0
  28. scrapely_client/_resource_clients/run_collection.py +226 -0
  29. scrapely_client/_resource_clients/schedule.py +243 -0
  30. scrapely_client/_resource_clients/schedule_collection.py +260 -0
  31. scrapely_client/_resource_clients/store_collection.py +242 -0
  32. scrapely_client/_resource_clients/task.py +793 -0
  33. scrapely_client/_resource_clients/task_collection.py +322 -0
  34. scrapely_client/_resource_clients/user.py +262 -0
  35. scrapely_client/_resource_clients/webhook.py +292 -0
  36. scrapely_client/_resource_clients/webhook_collection.py +281 -0
  37. scrapely_client/_resource_clients/webhook_dispatch.py +84 -0
  38. scrapely_client/_resource_clients/webhook_dispatch_collection.py +158 -0
  39. scrapely_client/_scrapely_client.py +837 -0
  40. scrapely_client/_statistics.py +29 -0
  41. scrapely_client/_status_message_watcher.py +197 -0
  42. scrapely_client/_streamed_log.py +250 -0
  43. scrapely_client/_typeddicts.py +408 -0
  44. scrapely_client/_utils/__init__.py +0 -0
  45. scrapely_client/_utils/crypto.py +87 -0
  46. scrapely_client/_utils/encoding.py +133 -0
  47. scrapely_client/_utils/errors.py +33 -0
  48. scrapely_client/_utils/http.py +137 -0
  49. scrapely_client/_utils/time.py +32 -0
  50. scrapely_client/_utils/try_import.py +61 -0
  51. scrapely_client/errors.py +190 -0
  52. scrapely_client/http_clients/__init__.py +10 -0
  53. scrapely_client/http_clients/_base.py +430 -0
  54. scrapely_client/http_clients/_impit.py +598 -0
  55. scrapely_client/http_compressors/__init__.py +18 -0
  56. scrapely_client/http_compressors/_base.py +25 -0
  57. scrapely_client/http_compressors/_brotli.py +38 -0
  58. scrapely_client/http_compressors/_gzip.py +38 -0
  59. scrapely_client/http_compressors/_resolve.py +38 -0
  60. scrapely_client/py.typed +0 -0
  61. scrapely_client/types.py +52 -0
  62. scrapely_client_python-3.1.3.dist-info/METADATA +269 -0
  63. scrapely_client_python-3.1.3.dist-info/RECORD +65 -0
  64. scrapely_client_python-3.1.3.dist-info/WHEEL +4 -0
  65. scrapely_client_python-3.1.3.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,875 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import random
5
+ import string
6
+ import time
7
+ from datetime import timedelta
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from scrapely_client._docs import docs_group
11
+ from scrapely_client._logging import create_redirect_logger
12
+ from scrapely_client._models import Run, RunResponse
13
+ from scrapely_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
14
+ from scrapely_client._status_message_watcher import StatusMessageWatcher, StatusMessageWatcherAsync
15
+ from scrapely_client._streamed_log import StreamedLog, StreamedLogAsync
16
+ from scrapely_client._utils.encoding import encode_key_value_store_record_value
17
+ from scrapely_client._utils.http import response_to_dict, to_safe_id
18
+ from scrapely_client._utils.time import to_seconds
19
+
20
+ if TYPE_CHECKING:
21
+ import logging
22
+ from decimal import Decimal
23
+
24
+ from scrapely_client._literals import GeneralAccess
25
+ from scrapely_client._resource_clients import (
26
+ DatasetClient,
27
+ DatasetClientAsync,
28
+ KeyValueStoreClient,
29
+ KeyValueStoreClientAsync,
30
+ LogClient,
31
+ LogClientAsync,
32
+ RequestQueueClient,
33
+ RequestQueueClientAsync,
34
+ )
35
+ from scrapely_client.types import Timeout
36
+
37
+
38
+ @docs_group('Resource clients')
39
+ class RunClient(ResourceClient):
40
+ """Sub-client for managing a specific Actor run.
41
+
42
+ Provides methods to manage a specific Actor run, e.g. get it, update it, abort it, or wait for it to finish.
43
+ Obtain an instance via an appropriate method on the `ApifyClient` class.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ *,
49
+ resource_id: str,
50
+ resource_path: str = 'actor-runs',
51
+ **kwargs: Any,
52
+ ) -> None:
53
+ super().__init__(
54
+ resource_id=resource_id,
55
+ resource_path=resource_path,
56
+ **kwargs,
57
+ )
58
+
59
+ def get(self, *, timeout: Timeout = 'short') -> Run | None:
60
+ """Return information about the Actor run.
61
+
62
+
63
+ Args:
64
+ timeout: Timeout for the API HTTP request.
65
+
66
+ Returns:
67
+ The retrieved Actor run data.
68
+ """
69
+ result = self._get(timeout=timeout)
70
+ if result is None:
71
+ return None
72
+ return RunResponse.model_validate(result).data
73
+
74
+ def update(
75
+ self,
76
+ *,
77
+ status_message: str | None = None,
78
+ is_status_message_terminal: bool | None = None,
79
+ general_access: GeneralAccess | None = None,
80
+ timeout: Timeout = 'short',
81
+ ) -> Run:
82
+ """Update the run with the specified fields.
83
+
84
+
85
+ Args:
86
+ status_message: The new status message for the run.
87
+ is_status_message_terminal: Set this flag to True if this is the final status message of the Actor run.
88
+ general_access: Determines how others can access the run and its storages.
89
+ timeout: Timeout for the API HTTP request.
90
+
91
+ Returns:
92
+ The updated run.
93
+ """
94
+ result = self._update(
95
+ timeout=timeout,
96
+ statusMessage=status_message,
97
+ isStatusMessageTerminal=is_status_message_terminal,
98
+ generalAccess=general_access,
99
+ )
100
+ return RunResponse.model_validate(result).data
101
+
102
+ def delete(self, *, timeout: Timeout = 'short') -> None:
103
+ """Delete the run.
104
+
105
+
106
+ Args:
107
+ timeout: Timeout for the API HTTP request.
108
+ """
109
+ self._delete(timeout=timeout)
110
+
111
+ def abort(self, *, gracefully: bool | None = None, timeout: Timeout = 'medium') -> Run:
112
+ """Abort the Actor run which is starting or currently running and return its details.
113
+
114
+
115
+ Args:
116
+ gracefully: If True, the Actor run will abort gracefully. It will send `aborting` and `persistStates`
117
+ events into the run and force-stop the run after 30 seconds. It is helpful in cases where you plan
118
+ to resurrect the run later.
119
+ timeout: Timeout for the API HTTP request.
120
+
121
+ Returns:
122
+ The data of the aborted Actor run.
123
+ """
124
+ response = self._http_client.call(
125
+ url=self._build_url('abort'),
126
+ method='POST',
127
+ params=self._build_params(gracefully=gracefully),
128
+ timeout=timeout,
129
+ )
130
+ result = response_to_dict(response)
131
+ return RunResponse.model_validate(result).data
132
+
133
+ def wait_for_finish(
134
+ self,
135
+ *,
136
+ wait_duration: timedelta | None = None,
137
+ timeout: Timeout = 'no_timeout',
138
+ ) -> Run | None:
139
+ """Wait synchronously until the run finishes or the server times out.
140
+
141
+ Args:
142
+ wait_duration: How long does the client wait for run to finish. None for indefinite.
143
+ timeout: Timeout for the API HTTP request.
144
+
145
+ Returns:
146
+ The Actor run data. If the status on the object is not one of the terminal statuses (SUCCEEDED, FAILED,
147
+ TIMED-OUT, ABORTED), then the run has not yet finished.
148
+ """
149
+ response = self._wait_for_finish(
150
+ url=self._build_url(),
151
+ params=self._build_params(),
152
+ wait_duration=wait_duration,
153
+ timeout=timeout,
154
+ )
155
+
156
+ if response is None:
157
+ return None
158
+
159
+ return Run.model_validate(response)
160
+
161
+ def metamorph(
162
+ self,
163
+ *,
164
+ target_actor_id: str,
165
+ target_actor_build: str | None = None,
166
+ run_input: Any = None,
167
+ content_type: str | None = None,
168
+ timeout: Timeout = 'medium',
169
+ ) -> Run:
170
+ """Transform an Actor run into a run of another Actor with a new input.
171
+
172
+
173
+ Args:
174
+ target_actor_id: ID of the target Actor that the run should be transformed into.
175
+ target_actor_build: The build of the target Actor. It can be either a build tag or build number.
176
+ By default, the run uses the build specified in the default run configuration for the target Actor
177
+ (typically the latest build).
178
+ run_input: The input to pass to the new run.
179
+ content_type: The content type of the input.
180
+ timeout: Timeout for the API HTTP request.
181
+
182
+ Returns:
183
+ The Actor run data.
184
+ """
185
+ run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)
186
+
187
+ safe_target_actor_id = to_safe_id(target_actor_id)
188
+
189
+ request_params = self._build_params(targetActorId=safe_target_actor_id, build=target_actor_build)
190
+
191
+ response = self._http_client.call(
192
+ url=self._build_url('metamorph'),
193
+ method='POST',
194
+ headers={'content-type': content_type},
195
+ data=run_input,
196
+ params=request_params,
197
+ timeout=timeout,
198
+ )
199
+
200
+ result = response_to_dict(response)
201
+ return RunResponse.model_validate(result).data
202
+
203
+ def resurrect(
204
+ self,
205
+ *,
206
+ build: str | None = None,
207
+ memory_mbytes: int | None = None,
208
+ run_timeout: timedelta | None = None,
209
+ max_items: int | None = None,
210
+ max_total_charge_usd: Decimal | None = None,
211
+ restart_on_error: bool | None = None,
212
+ timeout: Timeout = 'medium',
213
+ ) -> Run:
214
+ """Resurrect a finished Actor run.
215
+
216
+ Only finished runs, i.e. runs with status SUCCEEDED, FAILED, ABORTED and TIMED-OUT can be resurrected.
217
+ Run status will be updated to RUNNING and its container will be restarted with the same default storages.
218
+
219
+
220
+ Args:
221
+ build: Which Actor build the resurrected run should use. It can be either a build tag or build number.
222
+ By default, the resurrected run uses the same build as before.
223
+ memory_mbytes: New memory limit for the resurrected run, in megabytes. By default, the resurrected run
224
+ uses the same memory limit as before.
225
+ run_timeout: New timeout for the resurrected run. By default, the resurrected run uses the
226
+ same timeout as before.
227
+ max_items: Maximum number of items that the resurrected pay-per-result run will return. By default, the
228
+ resurrected run uses the same limit as before. Limit can be only increased.
229
+ max_total_charge_usd: Maximum cost for the resurrected pay-per-event run in USD. By default, the
230
+ resurrected run uses the same limit as before. Limit can be only increased.
231
+ restart_on_error: Determines whether the resurrected run will be restarted if it fails.
232
+ By default, the resurrected run uses the same setting as before.
233
+ timeout: Timeout for the API HTTP request.
234
+
235
+ Returns:
236
+ The Actor run data.
237
+ """
238
+ request_params = self._build_params(
239
+ build=build,
240
+ memory=memory_mbytes,
241
+ timeout=to_seconds(run_timeout, as_int=True),
242
+ maxItems=max_items,
243
+ maxTotalChargeUsd=max_total_charge_usd,
244
+ restartOnError=restart_on_error,
245
+ )
246
+
247
+ response = self._http_client.call(
248
+ url=self._build_url('resurrect'),
249
+ method='POST',
250
+ params=request_params,
251
+ timeout=timeout,
252
+ )
253
+
254
+ result = response_to_dict(response)
255
+ return RunResponse.model_validate(result).data
256
+
257
+ def reboot(self, *, timeout: Timeout = 'medium') -> Run:
258
+ """Reboot an Actor run. Only runs that are running, i.e. runs with status RUNNING can be rebooted.
259
+
260
+
261
+ Args:
262
+ timeout: Timeout for the API HTTP request.
263
+
264
+ Returns:
265
+ The Actor run data.
266
+ """
267
+ response = self._http_client.call(
268
+ url=self._build_url('reboot'),
269
+ method='POST',
270
+ timeout=timeout,
271
+ )
272
+ result = response_to_dict(response)
273
+ return RunResponse.model_validate(result).data
274
+
275
+ def dataset(self) -> DatasetClient:
276
+ """Get the client for the default dataset of the Actor run.
277
+
278
+
279
+ Returns:
280
+ A client allowing access to the default dataset of this Actor run.
281
+ """
282
+ return self._client_registry.dataset_client(
283
+ resource_path='dataset',
284
+ params=self._default_params,
285
+ **self._base_client_kwargs,
286
+ )
287
+
288
+ def key_value_store(self) -> KeyValueStoreClient:
289
+ """Get the client for the default key-value store of the Actor run.
290
+
291
+
292
+ Returns:
293
+ A client allowing access to the default key-value store of this Actor run.
294
+ """
295
+ return self._client_registry.key_value_store_client(
296
+ resource_path='key-value-store',
297
+ params=self._default_params,
298
+ **self._base_client_kwargs,
299
+ )
300
+
301
+ def request_queue(self) -> RequestQueueClient:
302
+ """Get the client for the default request queue of the Actor run.
303
+
304
+
305
+ Returns:
306
+ A client allowing access to the default request_queue of this Actor run.
307
+ """
308
+ return self._client_registry.request_queue_client(
309
+ resource_path='request-queue',
310
+ params=self._default_params,
311
+ **self._base_client_kwargs,
312
+ )
313
+
314
+ def log(self) -> LogClient:
315
+ """Get the client for the log of the Actor run.
316
+
317
+
318
+ Returns:
319
+ A client allowing access to the log of this Actor run.
320
+ """
321
+ return self._client_registry.log_client(
322
+ resource_path='log',
323
+ params=self._default_params,
324
+ **self._base_client_kwargs,
325
+ )
326
+
327
+ def get_streamed_log(
328
+ self,
329
+ to_logger: logging.Logger | None = None,
330
+ *,
331
+ from_start: bool = True,
332
+ timeout: Timeout = 'long',
333
+ ) -> StreamedLog:
334
+ """Get `StreamedLog` instance that can be used to redirect logs.
335
+
336
+ `StreamedLog` can be explicitly started and stopped or used as a context manager.
337
+
338
+ Args:
339
+ to_logger: `Logger` used for logging the redirected messages. If not provided, a new logger is created
340
+ from_start: If `True`, all logs from the start of the Actor run will be redirected. If `False`, only newly
341
+ arrived logs will be redirected. This can be useful for redirecting only a small portion of relevant
342
+ logs for long-running Actors in stand-by.
343
+ timeout: Timeout for the API HTTP request.
344
+
345
+ Returns:
346
+ `StreamedLog` instance for redirected logs.
347
+ """
348
+ run_data = self.get(timeout=timeout)
349
+ run_id = f'runId:{run_data.id}' if run_data and run_data.id else ''
350
+
351
+ actor_id = run_data.act_id if run_data else ''
352
+ actor_data = None
353
+ if actor_id:
354
+ actor_client = self._client_registry.actor_client(
355
+ resource_id=actor_id,
356
+ base_url=self._base_url,
357
+ public_base_url=self._public_base_url,
358
+ http_client=self._http_client,
359
+ client_registry=self._client_registry,
360
+ )
361
+ actor_data = actor_client.get(timeout=timeout)
362
+ actor_name = actor_data.name if actor_data else ''
363
+
364
+ if not to_logger:
365
+ name = ' '.join(part for part in (actor_name, run_id) if part)
366
+ to_logger = create_redirect_logger(f'apify.{name}')
367
+
368
+ return StreamedLog(log_client=self.log(), to_logger=to_logger, from_start=from_start)
369
+
370
+ def charge(
371
+ self,
372
+ event_name: str,
373
+ *,
374
+ count: int = 1,
375
+ idempotency_key: str | None = None,
376
+ timeout: Timeout = 'short',
377
+ ) -> None:
378
+ """Charge for an event of a Pay-Per-Event Actor run.
379
+
380
+
381
+ Args:
382
+ event_name: The name of the event to charge for.
383
+ count: The number of events to charge.
384
+ idempotency_key: A unique key to ensure idempotent charging. If not provided,
385
+ one will be auto-generated.
386
+ timeout: Timeout for the API HTTP request.
387
+
388
+ Raises:
389
+ ValueError: If event_name is empty or not provided.
390
+ """
391
+ if not event_name:
392
+ raise ValueError('event_name is required for charging an event')
393
+
394
+ if idempotency_key is None:
395
+ random_suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=6))
396
+ timestamp_ms = int(time.time() * 1000)
397
+ idempotency_key = f'{self._resource_id}-{event_name}-{timestamp_ms}-{random_suffix}'
398
+
399
+ self._http_client.call(
400
+ url=self._build_url('charge'),
401
+ method='POST',
402
+ headers={
403
+ 'idempotency-key': idempotency_key,
404
+ 'content-type': 'application/json',
405
+ },
406
+ data=json.dumps(
407
+ {
408
+ 'eventName': event_name,
409
+ 'count': count,
410
+ }
411
+ ),
412
+ timeout=timeout,
413
+ )
414
+
415
+ def get_status_message_watcher(
416
+ self,
417
+ *,
418
+ to_logger: logging.Logger | None = None,
419
+ check_period: timedelta = timedelta(seconds=1),
420
+ timeout: Timeout = 'long',
421
+ ) -> StatusMessageWatcher:
422
+ """Get `StatusMessageWatcher` instance that can be used to redirect status and status messages to logs.
423
+
424
+ `StatusMessageWatcher` can be explicitly started and stopped or used as a context manager.
425
+
426
+ Args:
427
+ to_logger: `Logger` used for logging the status and status messages. If not provided, a new logger is
428
+ created.
429
+ check_period: The period with which the status message will be polled.
430
+ timeout: Timeout for the API HTTP request.
431
+
432
+ Returns:
433
+ `StatusMessageWatcher` instance.
434
+ """
435
+ run_data = self.get(timeout=timeout)
436
+ run_id = f'runId:{run_data.id}' if run_data and run_data.id else ''
437
+
438
+ actor_id = run_data.act_id if run_data else ''
439
+ actor_data = None
440
+ if actor_id:
441
+ actor_client = self._client_registry.actor_client(
442
+ resource_id=actor_id,
443
+ base_url=self._base_url,
444
+ public_base_url=self._public_base_url,
445
+ http_client=self._http_client,
446
+ client_registry=self._client_registry,
447
+ )
448
+ actor_data = actor_client.get(timeout=timeout)
449
+ actor_name = actor_data.name if actor_data else ''
450
+
451
+ if not to_logger:
452
+ name = ' '.join(part for part in (actor_name, run_id) if part)
453
+ to_logger = create_redirect_logger(f'apify.{name}')
454
+
455
+ return StatusMessageWatcher(run_client=self, to_logger=to_logger, check_period=check_period)
456
+
457
+
458
+ @docs_group('Resource clients')
459
+ class RunClientAsync(ResourceClientAsync):
460
+ """Sub-client for managing a specific Actor run.
461
+
462
+ Provides methods to manage a specific Actor run, e.g. get it, update it, abort it, or wait for it to finish.
463
+ Obtain an instance via an appropriate method on the `ApifyClientAsync` class.
464
+ """
465
+
466
+ def __init__(
467
+ self,
468
+ *,
469
+ resource_id: str,
470
+ resource_path: str = 'actor-runs',
471
+ **kwargs: Any,
472
+ ) -> None:
473
+ super().__init__(
474
+ resource_id=resource_id,
475
+ resource_path=resource_path,
476
+ **kwargs,
477
+ )
478
+
479
+ async def get(self, *, timeout: Timeout = 'short') -> Run | None:
480
+ """Return information about the Actor run.
481
+
482
+
483
+ Args:
484
+ timeout: Timeout for the API HTTP request.
485
+
486
+ Returns:
487
+ The retrieved Actor run data.
488
+ """
489
+ result = await self._get(timeout=timeout)
490
+ if result is None:
491
+ return None
492
+ return RunResponse.model_validate(result).data
493
+
494
+ async def update(
495
+ self,
496
+ *,
497
+ status_message: str | None = None,
498
+ is_status_message_terminal: bool | None = None,
499
+ general_access: GeneralAccess | None = None,
500
+ timeout: Timeout = 'short',
501
+ ) -> Run:
502
+ """Update the run with the specified fields.
503
+
504
+
505
+ Args:
506
+ status_message: The new status message for the run.
507
+ is_status_message_terminal: Set this flag to True if this is the final status message of the Actor run.
508
+ general_access: Determines how others can access the run and its storages.
509
+ timeout: Timeout for the API HTTP request.
510
+
511
+ Returns:
512
+ The updated run.
513
+ """
514
+ result = await self._update(
515
+ timeout=timeout,
516
+ statusMessage=status_message,
517
+ isStatusMessageTerminal=is_status_message_terminal,
518
+ generalAccess=general_access,
519
+ )
520
+ return RunResponse.model_validate(result).data
521
+
522
+ async def abort(self, *, gracefully: bool | None = None, timeout: Timeout = 'medium') -> Run:
523
+ """Abort the Actor run which is starting or currently running and return its details.
524
+
525
+
526
+ Args:
527
+ gracefully: If True, the Actor run will abort gracefully. It will send `aborting` and `persistStates`
528
+ events into the run and force-stop the run after 30 seconds. It is helpful in cases where you plan
529
+ to resurrect the run later.
530
+ timeout: Timeout for the API HTTP request.
531
+
532
+ Returns:
533
+ The data of the aborted Actor run.
534
+ """
535
+ response = await self._http_client.call(
536
+ url=self._build_url('abort'),
537
+ method='POST',
538
+ params=self._build_params(gracefully=gracefully),
539
+ timeout=timeout,
540
+ )
541
+ result = response_to_dict(response)
542
+ return RunResponse.model_validate(result).data
543
+
544
+ async def wait_for_finish(
545
+ self,
546
+ *,
547
+ wait_duration: timedelta | None = None,
548
+ timeout: Timeout = 'no_timeout',
549
+ ) -> Run | None:
550
+ """Wait asynchronously until the run finishes or the server times out.
551
+
552
+ Args:
553
+ wait_duration: How long does the client wait for run to finish. None for indefinite.
554
+ timeout: Timeout for the API HTTP request.
555
+
556
+ Returns:
557
+ The Actor run data. If the status on the object is not one of the terminal statuses (SUCCEEDED, FAILED,
558
+ TIMED-OUT, ABORTED), then the run has not yet finished.
559
+ """
560
+ response = await self._wait_for_finish(
561
+ url=self._build_url(),
562
+ params=self._build_params(),
563
+ wait_duration=wait_duration,
564
+ timeout=timeout,
565
+ )
566
+ return Run.model_validate(response) if response is not None else None
567
+
568
+ async def delete(self, *, timeout: Timeout = 'short') -> None:
569
+ """Delete the run.
570
+
571
+
572
+ Args:
573
+ timeout: Timeout for the API HTTP request.
574
+ """
575
+ await self._delete(timeout=timeout)
576
+
577
+ async def metamorph(
578
+ self,
579
+ *,
580
+ target_actor_id: str,
581
+ target_actor_build: str | None = None,
582
+ run_input: Any = None,
583
+ content_type: str | None = None,
584
+ timeout: Timeout = 'medium',
585
+ ) -> Run:
586
+ """Transform an Actor run into a run of another Actor with a new input.
587
+
588
+
589
+ Args:
590
+ target_actor_id: ID of the target Actor that the run should be transformed into.
591
+ target_actor_build: The build of the target Actor. It can be either a build tag or build number.
592
+ By default, the run uses the build specified in the default run configuration for the target Actor
593
+ (typically the latest build).
594
+ run_input: The input to pass to the new run.
595
+ content_type: The content type of the input.
596
+ timeout: Timeout for the API HTTP request.
597
+
598
+ Returns:
599
+ The Actor run data.
600
+ """
601
+ run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)
602
+
603
+ safe_target_actor_id = to_safe_id(target_actor_id)
604
+
605
+ request_params = self._build_params(
606
+ targetActorId=safe_target_actor_id,
607
+ build=target_actor_build,
608
+ )
609
+
610
+ response = await self._http_client.call(
611
+ url=self._build_url('metamorph'),
612
+ method='POST',
613
+ headers={'content-type': content_type},
614
+ data=run_input,
615
+ params=request_params,
616
+ timeout=timeout,
617
+ )
618
+
619
+ result = response_to_dict(response)
620
+ return RunResponse.model_validate(result).data
621
+
622
+ async def resurrect(
623
+ self,
624
+ *,
625
+ build: str | None = None,
626
+ memory_mbytes: int | None = None,
627
+ run_timeout: timedelta | None = None,
628
+ max_items: int | None = None,
629
+ max_total_charge_usd: Decimal | None = None,
630
+ restart_on_error: bool | None = None,
631
+ timeout: Timeout = 'medium',
632
+ ) -> Run:
633
+ """Resurrect a finished Actor run.
634
+
635
+ Only finished runs, i.e. runs with status SUCCEEDED, FAILED, ABORTED and TIMED-OUT can be resurrected.
636
+ Run status will be updated to RUNNING and its container will be restarted with the same default storages.
637
+
638
+
639
+ Args:
640
+ build: Which Actor build the resurrected run should use. It can be either a build tag or build number.
641
+ By default, the resurrected run uses the same build as before.
642
+ memory_mbytes: New memory limit for the resurrected run, in megabytes. By default, the resurrected run
643
+ uses the same memory limit as before.
644
+ run_timeout: New timeout for the resurrected run. By default, the resurrected run uses the
645
+ same timeout as before.
646
+ max_items: Maximum number of items that the resurrected pay-per-result run will return. By default, the
647
+ resurrected run uses the same limit as before. Limit can be only increased.
648
+ max_total_charge_usd: Maximum cost for the resurrected pay-per-event run in USD. By default, the
649
+ resurrected run uses the same limit as before. Limit can be only increased.
650
+ restart_on_error: Determines whether the resurrected run will be restarted if it fails.
651
+ By default, the resurrected run uses the same setting as before.
652
+ timeout: Timeout for the API HTTP request.
653
+
654
+ Returns:
655
+ The Actor run data.
656
+ """
657
+ request_params = self._build_params(
658
+ build=build,
659
+ memory=memory_mbytes,
660
+ timeout=to_seconds(run_timeout, as_int=True),
661
+ maxItems=max_items,
662
+ maxTotalChargeUsd=max_total_charge_usd,
663
+ restartOnError=restart_on_error,
664
+ )
665
+
666
+ response = await self._http_client.call(
667
+ url=self._build_url('resurrect'),
668
+ method='POST',
669
+ params=request_params,
670
+ timeout=timeout,
671
+ )
672
+
673
+ result = response_to_dict(response)
674
+ return RunResponse.model_validate(result).data
675
+
676
+ async def reboot(self, *, timeout: Timeout = 'medium') -> Run:
677
+ """Reboot an Actor run. Only runs that are running, i.e. runs with status RUNNING can be rebooted.
678
+
679
+
680
+ Args:
681
+ timeout: Timeout for the API HTTP request.
682
+
683
+ Returns:
684
+ The Actor run data.
685
+ """
686
+ response = await self._http_client.call(
687
+ url=self._build_url('reboot'),
688
+ method='POST',
689
+ timeout=timeout,
690
+ )
691
+ result = response_to_dict(response)
692
+ return RunResponse.model_validate(result).data
693
+
694
+ def dataset(self) -> DatasetClientAsync:
695
+ """Get the client for the default dataset of the Actor run.
696
+
697
+
698
+ Returns:
699
+ A client allowing access to the default dataset of this Actor run.
700
+ """
701
+ return self._client_registry.dataset_client(
702
+ resource_path='dataset',
703
+ params=self._default_params,
704
+ **self._base_client_kwargs,
705
+ )
706
+
707
+ def key_value_store(self) -> KeyValueStoreClientAsync:
708
+ """Get the client for the default key-value store of the Actor run.
709
+
710
+
711
+ Returns:
712
+ A client allowing access to the default key-value store of this Actor run.
713
+ """
714
+ return self._client_registry.key_value_store_client(
715
+ resource_path='key-value-store',
716
+ params=self._default_params,
717
+ **self._base_client_kwargs,
718
+ )
719
+
720
+ def request_queue(self) -> RequestQueueClientAsync:
721
+ """Get the client for the default request queue of the Actor run.
722
+
723
+
724
+ Returns:
725
+ A client allowing access to the default request_queue of this Actor run.
726
+ """
727
+ return self._client_registry.request_queue_client(
728
+ resource_path='request-queue',
729
+ params=self._default_params,
730
+ **self._base_client_kwargs,
731
+ )
732
+
733
+ def log(self) -> LogClientAsync:
734
+ """Get the client for the log of the Actor run.
735
+
736
+
737
+ Returns:
738
+ A client allowing access to the log of this Actor run.
739
+ """
740
+ return self._client_registry.log_client(
741
+ resource_path='log',
742
+ params=self._default_params,
743
+ **self._base_client_kwargs,
744
+ )
745
+
746
+ async def get_streamed_log(
747
+ self,
748
+ to_logger: logging.Logger | None = None,
749
+ *,
750
+ from_start: bool = True,
751
+ timeout: Timeout = 'long',
752
+ ) -> StreamedLogAsync:
753
+ """Get `StreamedLog` instance that can be used to redirect logs.
754
+
755
+ `StreamedLog` can be explicitly started and stopped or used as a context manager.
756
+
757
+ Args:
758
+ to_logger: `Logger` used for logging the redirected messages. If not provided, a new logger is created
759
+ from_start: If `True`, all logs from the start of the Actor run will be redirected. If `False`, only newly
760
+ arrived logs will be redirected. This can be useful for redirecting only a small portion of relevant
761
+ logs for long-running Actors in stand-by.
762
+ timeout: Timeout for the API HTTP request.
763
+
764
+ Returns:
765
+ `StreamedLog` instance for redirected logs.
766
+ """
767
+ run_data = await self.get(timeout=timeout)
768
+ run_id = f'runId:{run_data.id}' if run_data and run_data.id else ''
769
+
770
+ actor_id = run_data.act_id if run_data else ''
771
+ actor_data = None
772
+ if actor_id:
773
+ actor_client = self._client_registry.actor_client(
774
+ resource_id=actor_id,
775
+ base_url=self._base_url,
776
+ public_base_url=self._public_base_url,
777
+ http_client=self._http_client,
778
+ client_registry=self._client_registry,
779
+ )
780
+ actor_data = await actor_client.get(timeout=timeout)
781
+ actor_name = actor_data.name if actor_data else ''
782
+
783
+ if not to_logger:
784
+ name = ' '.join(part for part in (actor_name, run_id) if part)
785
+ to_logger = create_redirect_logger(f'apify.{name}')
786
+
787
+ return StreamedLogAsync(log_client=self.log(), to_logger=to_logger, from_start=from_start)
788
+
789
+ async def charge(
790
+ self,
791
+ event_name: str,
792
+ *,
793
+ count: int = 1,
794
+ idempotency_key: str | None = None,
795
+ timeout: Timeout = 'short',
796
+ ) -> None:
797
+ """Charge for an event of a Pay-Per-Event Actor run.
798
+
799
+
800
+ Args:
801
+ event_name: The name of the event to charge for.
802
+ count: The number of events to charge.
803
+ idempotency_key: A unique key to ensure idempotent charging. If not provided,
804
+ one will be auto-generated.
805
+ timeout: Timeout for the API HTTP request.
806
+
807
+ Raises:
808
+ ValueError: If event_name is empty or not provided.
809
+ """
810
+ if not event_name:
811
+ raise ValueError('event_name is required for charging an event')
812
+
813
+ if idempotency_key is None:
814
+ random_suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=6))
815
+ timestamp_ms = int(time.time() * 1000)
816
+ idempotency_key = f'{self._resource_id}-{event_name}-{timestamp_ms}-{random_suffix}'
817
+
818
+ await self._http_client.call(
819
+ url=self._build_url('charge'),
820
+ method='POST',
821
+ headers={
822
+ 'idempotency-key': idempotency_key,
823
+ 'content-type': 'application/json',
824
+ },
825
+ data=json.dumps(
826
+ {
827
+ 'eventName': event_name,
828
+ 'count': count,
829
+ }
830
+ ),
831
+ timeout=timeout,
832
+ )
833
+
834
+ async def get_status_message_watcher(
835
+ self,
836
+ *,
837
+ to_logger: logging.Logger | None = None,
838
+ check_period: timedelta = timedelta(seconds=1),
839
+ timeout: Timeout = 'long',
840
+ ) -> StatusMessageWatcherAsync:
841
+ """Get `StatusMessageWatcher` instance that can be used to redirect status and status messages to logs.
842
+
843
+ `StatusMessageWatcher` can be explicitly started and stopped or used as a context manager.
844
+
845
+ Args:
846
+ to_logger: `Logger` used for logging the status and status messages. If not provided, a new logger is
847
+ created.
848
+ check_period: The period with which the status message will be polled.
849
+ timeout: Timeout for the API HTTP request.
850
+
851
+ Returns:
852
+ `StatusMessageWatcher` instance.
853
+ """
854
+ run_data = await self.get(timeout=timeout)
855
+
856
+ run_id = f'runId:{run_data.id}' if run_data and run_data.id else ''
857
+
858
+ actor_id = run_data.act_id if run_data else ''
859
+ actor_data = None
860
+ if actor_id:
861
+ actor_client = self._client_registry.actor_client(
862
+ resource_id=actor_id,
863
+ base_url=self._base_url,
864
+ public_base_url=self._public_base_url,
865
+ http_client=self._http_client,
866
+ client_registry=self._client_registry,
867
+ )
868
+ actor_data = await actor_client.get(timeout=timeout)
869
+ actor_name = actor_data.name if actor_data else ''
870
+
871
+ if not to_logger:
872
+ name = ' '.join(part for part in (actor_name, run_id) if part)
873
+ to_logger = create_redirect_logger(f'apify.{name}')
874
+
875
+ return StatusMessageWatcherAsync(run_client=self, to_logger=to_logger, check_period=check_period)