12
12
import pytest
13
13
from filelock import FileLock
14
14
15
- from apify_client import ApifyClientAsync
15
+ from apify_client import ApifyClient , ApifyClientAsync
16
16
from apify_shared .consts import ActorJobStatus , ActorSourceType , ApifyEnvVars
17
17
from crawlee import service_locator
18
18
from crawlee .storages import _creation_management
22
22
from apify ._models import ActorRun
23
23
24
24
if TYPE_CHECKING :
25
- from collections .abc import AsyncIterator , Awaitable , Coroutine , Mapping
25
+ from collections .abc import Awaitable , Coroutine , Iterator , Mapping
26
+ from decimal import Decimal
26
27
27
28
from apify_client .clients .resource_clients import ActorClientAsync
28
29
@@ -94,21 +95,27 @@ def _isolate_test_environment(prepare_test_env: Callable[[], None]) -> None:
94
95
prepare_test_env ()
95
96
96
97
98
+ @pytest .fixture (scope = 'session' )
99
+ def apify_token () -> str :
100
+ api_token = os .getenv (_TOKEN_ENV_VAR )
101
+
102
+ if not api_token :
103
+ raise RuntimeError (f'{ _TOKEN_ENV_VAR } environment variable is missing, cannot run tests!' )
104
+
105
+ return api_token
106
+
107
+
97
108
@pytest .fixture
98
- def apify_client_async () -> ApifyClientAsync :
109
+ def apify_client_async (apify_token : str ) -> ApifyClientAsync :
99
110
"""Create an instance of the ApifyClientAsync.
100
111
101
112
This fixture can't be session-scoped, because then you start getting `RuntimeError: Event loop is closed` errors,
102
113
because `httpx.AsyncClient` in `ApifyClientAsync` tries to reuse the same event loop across requests,
103
114
but `pytest-asyncio` closes the event loop after each test, and uses a new one for the next test.
104
115
"""
105
- api_token = os .getenv (_TOKEN_ENV_VAR )
106
116
api_url = os .getenv (_API_URL_ENV_VAR )
107
117
108
- if not api_token :
109
- raise RuntimeError (f'{ _TOKEN_ENV_VAR } environment variable is missing, cannot run tests!' )
110
-
111
- return ApifyClientAsync (api_token , api_url = api_url )
118
+ return ApifyClientAsync (apify_token , api_url = api_url )
112
119
113
120
114
121
@pytest .fixture (scope = 'session' )
@@ -217,17 +224,17 @@ def __call__(
217
224
"""
218
225
219
226
220
- @pytest .fixture
221
- async def make_actor (
227
+ @pytest .fixture ( scope = 'session' )
228
+ def make_actor (
222
229
actor_base_source_files : dict [str , str | bytes ],
223
- apify_client_async : ApifyClientAsync ,
224
- ) -> AsyncIterator [MakeActorFunction ]:
230
+ apify_token : str ,
231
+ ) -> Iterator [MakeActorFunction ]:
225
232
"""Fixture for creating temporary Actors for testing purposes.
226
233
227
234
This returns a function that creates a temporary Actor from the given main function or source files. The Actor
228
235
will be uploaded to the Apify Platform, built there, and after the test finishes, it will be automatically deleted.
229
236
"""
230
- actor_clients_for_cleanup : list [ActorClientAsync ] = []
237
+ actors_for_cleanup : list [str ] = []
231
238
232
239
async def _make_actor (
233
240
label : str ,
@@ -242,6 +249,7 @@ async def _make_actor(
242
249
if (main_func and main_py ) or (main_func and source_files ) or (main_py and source_files ):
243
250
raise TypeError ('Cannot specify more than one of `main_func`, `main_py` and `source_files` arguments' )
244
251
252
+ client = ApifyClientAsync (token = apify_token , api_url = os .getenv (_API_URL_ENV_VAR ))
245
253
actor_name = generate_unique_resource_name (label )
246
254
247
255
# Get the source of main_func and convert it into a reasonable main_py file.
@@ -290,7 +298,7 @@ async def _make_actor(
290
298
)
291
299
292
300
print (f'Creating Actor { actor_name } ...' )
293
- created_actor = await apify_client_async .actors ().create (
301
+ created_actor = await client .actors ().create (
294
302
name = actor_name ,
295
303
default_run_build = 'latest' ,
296
304
default_run_memory_mbytes = 256 ,
@@ -305,27 +313,41 @@ async def _make_actor(
305
313
],
306
314
)
307
315
308
- actor_client = apify_client_async .actor (created_actor ['id' ])
316
+ actor_client = client .actor (created_actor ['id' ])
309
317
310
318
print (f'Building Actor { actor_name } ...' )
311
319
build_result = await actor_client .build (version_number = '0.0' )
312
- build_client = apify_client_async .build (build_result ['id' ])
320
+ build_client = client .build (build_result ['id' ])
313
321
build_client_result = await build_client .wait_for_finish (wait_secs = 600 )
314
322
315
323
assert build_client_result is not None
316
324
assert build_client_result ['status' ] == ActorJobStatus .SUCCEEDED
317
325
318
326
# We only mark the client for cleanup if the build succeeded, so that if something goes wrong here,
319
327
# you have a chance to check the error.
320
- actor_clients_for_cleanup .append (actor_client )
328
+ actors_for_cleanup .append (created_actor [ 'id' ] )
321
329
322
330
return actor_client
323
331
324
332
yield _make_actor
325
333
334
+ client = ApifyClient (token = apify_token , api_url = os .getenv (_API_URL_ENV_VAR ))
335
+
326
336
# Delete all the generated Actors.
327
- for actor_client in actor_clients_for_cleanup :
328
- await actor_client .delete ()
337
+ for actor_id in actors_for_cleanup :
338
+ actor_client = client .actor (actor_id )
339
+
340
+ if (actor := actor_client .get ()) is not None :
341
+ actor_client .update (
342
+ pricing_infos = [
343
+ * actor ['pricingInfos' ],
344
+ {
345
+ 'pricingModel' : 'FREE' ,
346
+ },
347
+ ]
348
+ )
349
+
350
+ actor_client .delete ()
329
351
330
352
331
353
class RunActorFunction (Protocol ):
@@ -336,6 +358,7 @@ def __call__(
336
358
actor : ActorClientAsync ,
337
359
* ,
338
360
run_input : Any = None ,
361
+ max_total_charge_usd : Decimal | None = None ,
339
362
) -> Coroutine [None , None , ActorRun ]:
340
363
"""Initiate an Actor run and wait for its completion.
341
364
@@ -348,21 +371,30 @@ def __call__(
348
371
"""
349
372
350
373
351
- @pytest .fixture
352
- async def run_actor (apify_client_async : ApifyClientAsync ) -> RunActorFunction :
374
+ @pytest .fixture ( scope = 'session' )
375
+ def run_actor (apify_token : str ) -> RunActorFunction :
353
376
"""Fixture for calling an Actor run and waiting for its completion.
354
377
355
378
This fixture returns a function that initiates an Actor run with optional run input, waits for its completion,
356
379
and retrieves the final result. It uses the `wait_for_finish` method with a timeout of 10 minutes.
357
380
"""
358
381
359
- async def _run_actor (actor : ActorClientAsync , * , run_input : Any = None ) -> ActorRun :
360
- call_result = await actor .call (run_input = run_input )
382
+ async def _run_actor (
383
+ actor : ActorClientAsync ,
384
+ * ,
385
+ run_input : Any = None ,
386
+ max_total_charge_usd : Decimal | None = None ,
387
+ ) -> ActorRun :
388
+ call_result = await actor .call (
389
+ run_input = run_input ,
390
+ max_total_charge_usd = max_total_charge_usd ,
391
+ )
361
392
362
393
assert isinstance (call_result , dict ), 'The result of ActorClientAsync.call() is not a dictionary.'
363
394
assert 'id' in call_result , 'The result of ActorClientAsync.call() does not contain an ID.'
364
395
365
- run_client = apify_client_async .run (call_result ['id' ])
396
+ client = ApifyClientAsync (token = apify_token , api_url = os .getenv (_API_URL_ENV_VAR ))
397
+ run_client = client .run (call_result ['id' ])
366
398
run_result = await run_client .wait_for_finish (wait_secs = 600 )
367
399
368
400
return ActorRun .model_validate (run_result )
0 commit comments