Skip to content

Commit 2865b3e

Browse files
committed
api helpers and proxy config are ready
1 parent 7cea0b3 commit 2865b3e

11 files changed

+180
-186
lines changed

tests/integration/conftest.py

Lines changed: 98 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@
2929
SDK_ROOT_PATH = Path(__file__).parent.parent.parent.resolve()
3030

3131

32-
# To isolate the tests, we need to reset the used singletons before each test case
33-
# We also patch the default storage client with a tmp_path
3432
@pytest.fixture(autouse=True)
3533
def _reset_and_patch_default_instances() -> None:
34+
"""Reset the used singletons and patch the default storage client with a temporary directory.
35+
36+
To isolate the tests, we need to reset the used singletons before each test case. We also patch the default
37+
storage client with a tmp_path.
38+
"""
3639
from crawlee import service_container
3740

3841
cast(dict, service_container._services).clear()
@@ -41,13 +44,14 @@ def _reset_and_patch_default_instances() -> None:
4144
# TODO: StorageClientManager local storage client purge # noqa: TD003
4245

4346

44-
# This fixture can't be session-scoped,
45-
# because then you start getting `RuntimeError: Event loop is closed` errors,
46-
# because `httpx.AsyncClient` in `ApifyClientAsync` tries to reuse the same event loop across requests,
47-
# but `pytest-asyncio` closes the event loop after each test,
48-
# and uses a new one for the next test.
4947
@pytest.fixture
5048
def apify_client_async() -> ApifyClientAsync:
49+
"""Create an instance of the ApifyClientAsync.
50+
51+
This fixture can't be session-scoped, because then you start getting `RuntimeError: Event loop is closed` errors,
52+
because `httpx.AsyncClient` in `ApifyClientAsync` tries to reuse the same event loop across requests,
53+
but `pytest-asyncio` closes the event loop after each test, and uses a new one for the next test.
54+
"""
5155
api_token = os.getenv(TOKEN_ENV_VAR)
5256
api_url = os.getenv(API_URL_ENV_VAR)
5357

@@ -57,65 +61,26 @@ def apify_client_async() -> ApifyClientAsync:
5761
return ApifyClientAsync(api_token, api_url=api_url)
5862

5963

60-
class RunActorFunction(Protocol):
61-
"""A type for the `run_actor` fixture."""
62-
63-
def __call__(
64-
self,
65-
actor: ActorClientAsync,
66-
*,
67-
run_input: Any = None,
68-
) -> Coroutine[None, None, ActorRun]:
69-
"""Initiate an Actor run and wait for its completion.
70-
71-
Args:
72-
actor: Actor async client, in testing context usually created by `make_actor` fixture.
73-
run_input: Optional input for the Actor run.
74-
75-
Returns:
76-
Actor run result.
77-
"""
78-
79-
80-
@pytest.fixture
81-
async def run_actor(apify_client_async: ApifyClientAsync) -> RunActorFunction:
82-
"""Fixture for calling an Actor run and waiting for its completion.
83-
84-
This fixture returns a function that initiates an Actor run with optional run input, waits for its completion,
85-
and retrieves the final result. It uses the `wait_for_finish` method with a timeout of 10 minutes.
86-
87-
Returns:
88-
A coroutine that initiates an Actor run and waits for its completion.
89-
"""
90-
91-
async def _run_actor(actor: ActorClientAsync, *, run_input: Any = None) -> ActorRun:
92-
call_result = await actor.call(run_input=run_input)
93-
94-
assert isinstance(call_result, dict), 'The result of ActorClientAsync.call() is not a dictionary.'
95-
assert 'id' in call_result, 'The result of ActorClientAsync.call() does not contain an ID.'
96-
97-
run_client = apify_client_async.run(call_result['id'])
98-
run_result = await run_client.wait_for_finish(wait_secs=600)
99-
100-
return ActorRun.model_validate(run_result)
101-
102-
return _run_actor
103-
104-
105-
# Build the package wheel if it hasn't been built yet, and return the path to the wheel
10664
@pytest.fixture(scope='session')
10765
def sdk_wheel_path(tmp_path_factory: pytest.TempPathFactory, testrun_uid: str) -> Path:
66+
"""Build the package wheel if it hasn't been built yet, and return the path to the wheel."""
10867
# Make sure the wheel is not being built concurrently across all the pytest-xdist runners,
109-
# through locking the building process with a temp file
68+
# through locking the building process with a temp file.
11069
with FileLock(tmp_path_factory.getbasetemp().parent / 'sdk_wheel_build.lock'):
11170
# Make sure the wheel is built exactly once across across all the pytest-xdist runners,
112-
# through an indicator file saying that the wheel was already built
71+
# through an indicator file saying that the wheel was already built.
11372
was_wheel_built_this_test_run_file = tmp_path_factory.getbasetemp() / f'wheel_was_built_in_run_{testrun_uid}'
11473
if not was_wheel_built_this_test_run_file.exists():
115-
subprocess.run('python -m build', cwd=SDK_ROOT_PATH, shell=True, check=True, capture_output=True) # noqa: S602, S607
74+
subprocess.run(
75+
args='python -m build',
76+
cwd=SDK_ROOT_PATH,
77+
shell=True,
78+
check=True,
79+
capture_output=True,
80+
)
11681
was_wheel_built_this_test_run_file.touch()
11782

118-
# Read the current package version, necessary for getting the right wheel filename
83+
# Read the current package version, necessary for getting the right wheel filename.
11984
pyproject_toml_file = (SDK_ROOT_PATH / 'pyproject.toml').read_text(encoding='utf-8')
12085
for line in pyproject_toml_file.splitlines():
12186
if line.startswith('version = '):
@@ -127,7 +92,7 @@ def sdk_wheel_path(tmp_path_factory: pytest.TempPathFactory, testrun_uid: str) -
12792

12893
wheel_path = SDK_ROOT_PATH / 'dist' / f'apify-{sdk_version}-py3-none-any.whl'
12994

130-
# Just to be sure
95+
# Just to be sure.
13196
assert wheel_path.exists()
13297

13398
return wheel_path
@@ -173,56 +138,61 @@ def actor_base_source_files(sdk_wheel_path: Path) -> dict[str, str | bytes]:
173138
return source_files
174139

175140

176-
# Just a type for the make_actor result, so that we can import it in tests
177-
class ActorFactory(Protocol):
141+
class MakeActorFunction(Protocol):
142+
"""A type for the `make_actor` fixture."""
143+
178144
def __call__(
179145
self,
180-
actor_label: str,
146+
label: str,
181147
*,
182148
main_func: Callable | None = None,
183149
main_py: str | None = None,
184150
source_files: Mapping[str, str | bytes] | None = None,
185-
) -> Awaitable[ActorClientAsync]: ...
186-
187-
188-
@pytest.fixture
189-
async def make_actor(
190-
actor_base_source_files: dict[str, str | bytes],
191-
apify_client_async: ApifyClientAsync,
192-
) -> AsyncIterator[ActorFactory]:
193-
"""A fixture for returning a temporary Actor factory."""
194-
actor_clients_for_cleanup: list[ActorClientAsync] = []
195-
196-
async def _make_actor(
197-
actor_label: str,
198-
*,
199-
main_func: Callable | None = None,
200-
main_py: str | None = None,
201-
source_files: Mapping[str, str | bytes] | None = None,
202-
) -> ActorClientAsync:
203-
"""Create a temporary Actor from the given main function or source file(s).
151+
) -> Awaitable[ActorClientAsync]:
152+
"""Create a temporary Actor from the given main function or source files.
204153
205154
The Actor will be uploaded to the Apify Platform, built there, and after the test finishes, it will
206155
be automatically deleted.
207156
208157
You have to pass exactly one of the `main_func`, `main_py` and `source_files` arguments.
209158
210159
Args:
211-
actor_label: The label which will be a part of the generated Actor name
160+
label: The label which will be a part of the generated Actor name.
212161
main_func: The main function of the Actor.
213162
main_py: The `src/main.py` file of the Actor.
214163
source_files: A dictionary of the source files of the Actor.
215164
216165
Returns:
217166
A resource client for the created Actor.
218167
"""
168+
169+
170+
@pytest.fixture
171+
async def make_actor(
172+
actor_base_source_files: dict[str, str | bytes],
173+
apify_client_async: ApifyClientAsync,
174+
) -> AsyncIterator[MakeActorFunction]:
175+
"""Fixture for creating temporary Actors for testing purposes.
176+
177+
This returns a function that creates a temporary Actor from the given main function or source files. The Actor
178+
will be uploaded to the Apify Platform, built there, and after the test finishes, it will be automatically deleted.
179+
"""
180+
actor_clients_for_cleanup: list[ActorClientAsync] = []
181+
182+
async def _make_actor(
183+
label: str,
184+
*,
185+
main_func: Callable | None = None,
186+
main_py: str | None = None,
187+
source_files: Mapping[str, str | bytes] | None = None,
188+
) -> ActorClientAsync:
219189
if not (main_func or main_py or source_files):
220190
raise TypeError('One of `main_func`, `main_py` or `source_files` arguments must be specified')
221191

222192
if (main_func and main_py) or (main_func and source_files) or (main_py and source_files):
223193
raise TypeError('Cannot specify more than one of `main_func`, `main_py` and `source_files` arguments')
224194

225-
actor_name = generate_unique_resource_name(actor_label)
195+
actor_name = generate_unique_resource_name(label)
226196

227197
# Get the source of main_func and convert it into a reasonable main_py file.
228198
if main_func:
@@ -250,7 +220,7 @@ async def _make_actor(
250220
actor_source_files = actor_base_source_files.copy()
251221
actor_source_files.update(source_files)
252222

253-
# Reformat the source files in a format that the Apify API understands
223+
# Reformat the source files in a format that the Apify API understands.
254224
source_files_for_api = []
255225
for file_name, file_contents in actor_source_files.items():
256226
if isinstance(file_contents, str):
@@ -295,15 +265,56 @@ async def _make_actor(
295265
assert build_client_result is not None
296266
assert build_client_result['status'] == ActorJobStatus.SUCCEEDED
297267

298-
# We only mark the client for cleanup if the build succeeded,
299-
# so that if something goes wrong here,
300-
# you have a chance to check the error
268+
# We only mark the client for cleanup if the build succeeded, so that if something goes wrong here,
269+
# you have a chance to check the error.
301270
actor_clients_for_cleanup.append(actor_client)
302271

303272
return actor_client
304273

305274
yield _make_actor
306275

307-
# Delete all the generated actors
276+
# Delete all the generated Actors.
308277
for actor_client in actor_clients_for_cleanup:
309278
await actor_client.delete()
279+
280+
281+
class RunActorFunction(Protocol):
282+
"""A type for the `run_actor` fixture."""
283+
284+
def __call__(
285+
self,
286+
actor: ActorClientAsync,
287+
*,
288+
run_input: Any = None,
289+
) -> Coroutine[None, None, ActorRun]:
290+
"""Initiate an Actor run and wait for its completion.
291+
292+
Args:
293+
actor: Actor async client, in testing context usually created by `make_actor` fixture.
294+
run_input: Optional input for the Actor run.
295+
296+
Returns:
297+
Actor run result.
298+
"""
299+
300+
301+
@pytest.fixture
302+
async def run_actor(apify_client_async: ApifyClientAsync) -> RunActorFunction:
303+
"""Fixture for calling an Actor run and waiting for its completion.
304+
305+
This fixture returns a function that initiates an Actor run with optional run input, waits for its completion,
306+
and retrieves the final result. It uses the `wait_for_finish` method with a timeout of 10 minutes.
307+
"""
308+
309+
async def _run_actor(actor: ActorClientAsync, *, run_input: Any = None) -> ActorRun:
310+
call_result = await actor.call(run_input=run_input)
311+
312+
assert isinstance(call_result, dict), 'The result of ActorClientAsync.call() is not a dictionary.'
313+
assert 'id' in call_result, 'The result of ActorClientAsync.call() does not contain an ID.'
314+
315+
run_client = apify_client_async.run(call_result['id'])
316+
run_result = await run_client.wait_for_finish(wait_secs=600)
317+
318+
return ActorRun.model_validate(run_result)
319+
320+
return _run_actor

0 commit comments

Comments
 (0)