Skip to content

Commit cd2a5fc

Browse files
mvolfikfnesveda
andauthored
Add support for terminal status message, allow setting status message directly in exit (#88)
Depends on apify/apify-client-python#122 --------- Co-authored-by: František Nesveda <frantisek@apify.com>
1 parent 060175d commit cd2a5fc

File tree

9 files changed

+68
-21
lines changed

9 files changed

+68
-21
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Changelog
77
### Added
88

99
- option to add event handlers which accept no arguments
10+
- added support for `is_terminal` flag in status message update
11+
- option to set status message along with `Actor.exit()`
1012

1113
### Fixed
1214

docs/docs.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ and it should be called only once.
338338

339339
***
340340

341-
#### [](#actor-exit) `async Actor.exit(*, exit_code=0, event_listeners_timeout_secs=5)`
341+
#### [](#actor-exit) `async Actor.exit(*, exit_code=0, event_listeners_timeout_secs=5, status_message=None)`
342342

343343
Exit the actor instance.
344344

@@ -352,15 +352,17 @@ and stops the event manager.
352352

353353
* **exit_code** (`int`, *optional*) – The exit code with which the actor should fail (defaults to 0).
354354

355-
* **event_listeners_timeout_secs** (`float`, *optional*) – How long should the actor wait for actor event listeners to finish before exiting
355+
* **event_listeners_timeout_secs** (`float`, *optional*) – How long should the actor wait for actor event listeners to finish before exiting.
356+
357+
* **status_message** (`str`, *optional*) – The final status message that the actor should display.
356358

357359
* **Return type**
358360

359361
`None`
360362

361363
***
362364

363-
#### [](#actor-fail) `async Actor.fail(*, exit_code=1, exception=None)`
365+
#### [](#actor-fail) `async Actor.fail(*, exit_code=1, exception=None, status_message=None)`
364366

365367
Fail the actor instance.
366368

@@ -373,6 +375,8 @@ but it additionally sets the exit code to 1 (by default).
373375

374376
* **exception** (`BaseException`, *optional*) – The exception with which the actor failed.
375377

378+
* **status_message** (`str`, *optional*) – The final status message that the actor should display.
379+
376380
* **Return type**
377381

378382
`None`
@@ -908,14 +912,16 @@ For more information about Apify actor webhooks, please see the [documentation](
908912

909913
***
910914

911-
#### [](#actor-set_status_message) `async Actor.set_status_message(status_message)`
915+
#### [](#actor-set_status_message) `async Actor.set_status_message(status_message, *, is_terminal=None)`
912916

913917
Set the status message for the current actor run.
914918

915919
* **Parameters**
916920

917921
* **status_message** (`str`) – The status message to set to the run.
918922

923+
* **is_terminal** (`bool`, *optional*) – Set this flag to True if this is the final status message of the Actor run.
924+
919925
* **Returns**
920926

921927
The updated actor run object

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
install_requires=[
5757
'aiofiles ~= 22.1.0',
5858
'aioshutil ~= 1.2',
59-
'apify-client ~= 1.0.0',
59+
'apify-client ~= 1.1',
6060
'colorama ~= 0.4.6',
6161
'cryptography ~= 39.0.1',
6262
'httpx ~= 0.23.0',

src/apify/actor.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ async def exit(
301301
*,
302302
exit_code: int = 0,
303303
event_listeners_timeout_secs: Optional[float] = EVENT_LISTENERS_TIMEOUT_SECS,
304+
status_message: Optional[str] = None,
304305
) -> None:
305306
"""Exit the actor instance.
306307
@@ -312,18 +313,21 @@ async def exit(
312313
313314
Args:
314315
exit_code (int, optional): The exit code with which the actor should fail (defaults to `0`).
315-
event_listeners_timeout_secs (float, optional): How long should the actor wait for actor event listeners to finish before exiting
316+
event_listeners_timeout_secs (float, optional): How long should the actor wait for actor event listeners to finish before exiting.
317+
status_message (str, optional): The final status message that the actor should display.
316318
"""
317319
return await cls._get_default_instance().exit(
318320
exit_code=exit_code,
319321
event_listeners_timeout_secs=event_listeners_timeout_secs,
322+
status_message=status_message,
320323
)
321324

322325
async def _exit_internal(
323326
self,
324327
*,
325328
exit_code: int = 0,
326329
event_listeners_timeout_secs: Optional[float] = EVENT_LISTENERS_TIMEOUT_SECS,
330+
status_message: Optional[str] = None,
327331
) -> None:
328332
self._raise_if_not_initialized()
329333

@@ -340,6 +344,9 @@ async def _exit_internal(
340344
self._event_manager.emit(ActorEventTypes.PERSIST_STATE, {'isMigrating': False})
341345
self._was_final_persist_state_emitted = True
342346

347+
if status_message is not None:
348+
await self.set_status_message(status_message, is_terminal=True)
349+
343350
# Sleep for a bit so that the listeners have a chance to trigger
344351
await asyncio.sleep(0.1)
345352

@@ -362,6 +369,7 @@ async def fail(
362369
*,
363370
exit_code: int = 1,
364371
exception: Optional[BaseException] = None,
372+
status_message: Optional[str] = None,
365373
) -> None:
366374
"""Fail the actor instance.
367375
@@ -371,17 +379,20 @@ async def fail(
371379
Args:
372380
exit_code (int, optional): The exit code with which the actor should fail (defaults to `1`).
373381
exception (BaseException, optional): The exception with which the actor failed.
382+
status_message (str, optional): The final status message that the actor should display.
374383
"""
375384
return await cls._get_default_instance().fail(
376385
exit_code=exit_code,
377386
exception=exception,
387+
status_message=status_message,
378388
)
379389

380390
async def _fail_internal(
381391
self,
382392
*,
383393
exit_code: int = 1,
384394
exception: Optional[BaseException] = None,
395+
status_message: Optional[str] = None,
385396
) -> None:
386397
self._raise_if_not_initialized()
387398

@@ -390,7 +401,7 @@ async def _fail_internal(
390401
if exception and not _is_running_in_ipython():
391402
self.log.exception('Actor failed with an exception', exc_info=exception)
392403

393-
await self.exit(exit_code=exit_code)
404+
await self.exit(exit_code=exit_code, status_message=status_message)
394405

395406
@classmethod
396407
async def main(cls, main_actor_function: Callable[[], MainReturnType]) -> Optional[MainReturnType]:
@@ -1210,28 +1221,30 @@ async def _add_webhook_internal(
12101221
)
12111222

12121223
@classmethod
1213-
async def set_status_message(cls, status_message: str) -> Optional[Dict]:
1224+
async def set_status_message(cls, status_message: str, *, is_terminal: Optional[bool] = None) -> Optional[Dict]:
12141225
"""Set the status message for the current actor run.
12151226
12161227
Args:
12171228
status_message (str): The status message to set to the run.
1229+
is_terminal (bool, optional): Set this flag to True if this is the final status message of the Actor run.
12181230
12191231
Returns:
12201232
dict: The updated actor run object
12211233
"""
1222-
return await cls._get_default_instance().set_status_message(status_message=status_message)
1234+
return await cls._get_default_instance().set_status_message(status_message=status_message, is_terminal=is_terminal)
12231235

1224-
async def _set_status_message_internal(self, status_message: str) -> Optional[Dict]:
1236+
async def _set_status_message_internal(self, status_message: str, *, is_terminal: Optional[bool] = None) -> Optional[Dict]:
12251237
self._raise_if_not_initialized()
12261238

12271239
if not self.is_at_home():
1228-
self.log.error('Actor.set_status_message() is only supported when running on the Apify platform.')
1240+
title = 'Terminal status message' if is_terminal else 'Status message'
1241+
self.log.info(f'[{title}]: {status_message}')
12291242
return None
12301243

12311244
# If is_at_home() is True, config.actor_run_id is always set
12321245
assert self._config.actor_run_id is not None
12331246

1234-
return await self._apify_client.run(self._config.actor_run_id).update(status_message=status_message)
1247+
return await self._apify_client.run(self._config.actor_run_id).update(status_message=status_message, is_status_message_terminal=is_terminal)
12351248

12361249
@classmethod
12371250
async def create_proxy_configuration(

tests/integration/test_actor_api_helpers.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ class TestActorSetStatusMessage:
8181
async def test_actor_set_status_message(self, make_actor: ActorFactory) -> None:
8282
async def main() -> None:
8383
async with Actor:
84-
await Actor.set_status_message('testing-status-message')
84+
input = await Actor.get_input() or {}
85+
await Actor.set_status_message('testing-status-message', **input)
8586

8687
actor = await make_actor('set-status-message', main_func=main)
8788

@@ -90,6 +91,14 @@ async def main() -> None:
9091
assert run_result is not None
9192
assert run_result['status'] == 'SUCCEEDED'
9293
assert run_result['statusMessage'] == 'testing-status-message'
94+
assert run_result['isStatusMessageTerminal'] is None
95+
96+
run_result = await actor.call(run_input={'is_terminal': True})
97+
98+
assert run_result is not None
99+
assert run_result['status'] == 'SUCCEEDED'
100+
assert run_result['statusMessage'] == 'testing-status-message'
101+
assert run_result['isStatusMessageTerminal'] is True
93102

94103

95104
class TestActorStart:

tests/integration/test_actor_lifecycle.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ async def main() -> None:
8585
assert run_result['exitCode'] == exit_code
8686
assert run_result['status'] == 'FAILED'
8787

88+
# fail with status message
89+
run_result = await actor.call(run_input={'status_message': 'This is a test message'})
90+
assert run_result is not None
91+
assert run_result['status'] == 'FAILED'
92+
assert run_result.get('statusMessage') == 'This is a test message'
93+
8894
async def test_with_actor_fail_correctly(self, make_actor: ActorFactory) -> None:
8995
async def main() -> None:
9096
async with Actor:

tests/unit/actor/test_actor_helpers.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,20 @@ async def test_actor_add_webhook_not_work_locally(self, caplog: pytest.LogCaptur
102102
assert caplog.records[0].levelname == 'ERROR'
103103
assert 'Actor.add_webhook() is only supported when running on the Apify platform.' in caplog.records[0].message
104104

105-
async def test_actor_set_status_message_not_work_locally(self, caplog: pytest.LogCaptureFixture) -> None:
105+
async def test_actor_set_status_message_mock_locally(self, caplog: pytest.LogCaptureFixture) -> None:
106+
caplog.set_level('INFO')
106107
async with Actor() as my_actor:
107-
await my_actor.set_status_message('test')
108-
assert len(caplog.records) == 1
109-
assert caplog.records[0].levelname == 'ERROR'
110-
assert 'Actor.set_status_message() is only supported when running on the Apify platform.' in caplog.records[0].message
108+
await my_actor.set_status_message('test-status-message')
109+
matching_records = [record for record in caplog.records if 'test-status-message' in record.message]
110+
assert len(matching_records) == 1
111+
assert matching_records[0].levelname == 'INFO'
112+
assert '[Status message]: test-status-message' in matching_records[0].message
113+
114+
async def test_actor_set_status_message_terminal_mock_locally(self, caplog: pytest.LogCaptureFixture) -> None:
115+
caplog.set_level('INFO')
116+
async with Actor() as my_actor:
117+
await my_actor.fail(status_message='test-terminal-message')
118+
matching_records = [record for record in caplog.records if 'test-terminal-message' in record.message]
119+
assert len(matching_records) == 1
120+
assert matching_records[0].levelname == 'INFO'
121+
assert '[Terminal status message]: test-terminal-message' in matching_records[0].message

tests/unit/actor/test_actor_log.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
from apify import Actor, __version__
77
from apify.log import logger
8-
from apify_client._version import __version__ as apify_client_version
8+
from apify_client import __version__ as apify_client_version
99

1010

1111
class TestActorLog:
1212
async def test_actor_log(self, caplog: pytest.LogCaptureFixture) -> None:
13-
caplog.set_level(logging.DEBUG)
13+
caplog.set_level(logging.DEBUG, logger='apify')
1414
try:
1515
async with Actor:
1616
# Test Actor.log

tests/unit/test_event_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
class TestEventManagerLocal:
1919
async def test_lifecycle_local(self, caplog: pytest.LogCaptureFixture) -> None:
20-
caplog.set_level(logging.DEBUG)
20+
caplog.set_level(logging.DEBUG, logger='apify')
2121

2222
config = Configuration()
2323
event_manager = EventManager(config)

0 commit comments

Comments
 (0)