Skip to content

Commit 9c5c92a

Browse files
committed
Make make_actor fixture non-async and session-scoped, add PPE-related cleanup code
1 parent 36159b9 commit 9c5c92a

File tree

1 file changed

+56
-24
lines changed

1 file changed

+56
-24
lines changed

tests/integration/conftest.py

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import pytest
1313
from filelock import FileLock
1414

15-
from apify_client import ApifyClientAsync
15+
from apify_client import ApifyClient, ApifyClientAsync
1616
from apify_shared.consts import ActorJobStatus, ActorSourceType, ApifyEnvVars
1717
from crawlee import service_locator
1818
from crawlee.storages import _creation_management
@@ -22,7 +22,8 @@
2222
from apify._models import ActorRun
2323

2424
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
2627

2728
from apify_client.clients.resource_clients import ActorClientAsync
2829

@@ -94,21 +95,27 @@ def _isolate_test_environment(prepare_test_env: Callable[[], None]) -> None:
9495
prepare_test_env()
9596

9697

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+
97108
@pytest.fixture
98-
def apify_client_async() -> ApifyClientAsync:
109+
def apify_client_async(apify_token: str) -> ApifyClientAsync:
99110
"""Create an instance of the ApifyClientAsync.
100111
101112
This fixture can't be session-scoped, because then you start getting `RuntimeError: Event loop is closed` errors,
102113
because `httpx.AsyncClient` in `ApifyClientAsync` tries to reuse the same event loop across requests,
103114
but `pytest-asyncio` closes the event loop after each test, and uses a new one for the next test.
104115
"""
105-
api_token = os.getenv(_TOKEN_ENV_VAR)
106116
api_url = os.getenv(_API_URL_ENV_VAR)
107117

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)
112119

113120

114121
@pytest.fixture(scope='session')
@@ -217,17 +224,17 @@ def __call__(
217224
"""
218225

219226

220-
@pytest.fixture
221-
async def make_actor(
227+
@pytest.fixture(scope='session')
228+
def make_actor(
222229
actor_base_source_files: dict[str, str | bytes],
223-
apify_client_async: ApifyClientAsync,
224-
) -> AsyncIterator[MakeActorFunction]:
230+
apify_token: str,
231+
) -> Iterator[MakeActorFunction]:
225232
"""Fixture for creating temporary Actors for testing purposes.
226233
227234
This returns a function that creates a temporary Actor from the given main function or source files. The Actor
228235
will be uploaded to the Apify Platform, built there, and after the test finishes, it will be automatically deleted.
229236
"""
230-
actor_clients_for_cleanup: list[ActorClientAsync] = []
237+
actors_for_cleanup: list[str] = []
231238

232239
async def _make_actor(
233240
label: str,
@@ -242,6 +249,7 @@ async def _make_actor(
242249
if (main_func and main_py) or (main_func and source_files) or (main_py and source_files):
243250
raise TypeError('Cannot specify more than one of `main_func`, `main_py` and `source_files` arguments')
244251

252+
client = ApifyClientAsync(token=apify_token, api_url=os.getenv(_API_URL_ENV_VAR))
245253
actor_name = generate_unique_resource_name(label)
246254

247255
# Get the source of main_func and convert it into a reasonable main_py file.
@@ -290,7 +298,7 @@ async def _make_actor(
290298
)
291299

292300
print(f'Creating Actor {actor_name}...')
293-
created_actor = await apify_client_async.actors().create(
301+
created_actor = await client.actors().create(
294302
name=actor_name,
295303
default_run_build='latest',
296304
default_run_memory_mbytes=256,
@@ -305,27 +313,41 @@ async def _make_actor(
305313
],
306314
)
307315

308-
actor_client = apify_client_async.actor(created_actor['id'])
316+
actor_client = client.actor(created_actor['id'])
309317

310318
print(f'Building Actor {actor_name}...')
311319
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'])
313321
build_client_result = await build_client.wait_for_finish(wait_secs=600)
314322

315323
assert build_client_result is not None
316324
assert build_client_result['status'] == ActorJobStatus.SUCCEEDED
317325

318326
# We only mark the client for cleanup if the build succeeded, so that if something goes wrong here,
319327
# you have a chance to check the error.
320-
actor_clients_for_cleanup.append(actor_client)
328+
actors_for_cleanup.append(created_actor['id'])
321329

322330
return actor_client
323331

324332
yield _make_actor
325333

334+
client = ApifyClient(token=apify_token, api_url=os.getenv(_API_URL_ENV_VAR))
335+
326336
# 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()
329351

330352

331353
class RunActorFunction(Protocol):
@@ -336,6 +358,7 @@ def __call__(
336358
actor: ActorClientAsync,
337359
*,
338360
run_input: Any = None,
361+
max_total_charge_usd: Decimal | None = None,
339362
) -> Coroutine[None, None, ActorRun]:
340363
"""Initiate an Actor run and wait for its completion.
341364
@@ -348,21 +371,30 @@ def __call__(
348371
"""
349372

350373

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:
353376
"""Fixture for calling an Actor run and waiting for its completion.
354377
355378
This fixture returns a function that initiates an Actor run with optional run input, waits for its completion,
356379
and retrieves the final result. It uses the `wait_for_finish` method with a timeout of 10 minutes.
357380
"""
358381

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+
)
361392

362393
assert isinstance(call_result, dict), 'The result of ActorClientAsync.call() is not a dictionary.'
363394
assert 'id' in call_result, 'The result of ActorClientAsync.call() does not contain an ID.'
364395

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'])
366398
run_result = await run_client.wait_for_finish(wait_secs=600)
367399

368400
return ActorRun.model_validate(run_result)

0 commit comments

Comments
 (0)