From c83bfa78eb35d40e7ca008944c3ef39a35b365e9 Mon Sep 17 00:00:00 2001 From: Nick Frasser <1693461+nfrasser@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:47:52 -0400 Subject: [PATCH 01/10] feat(event): add/update event log with name Can use the event's ID as its name, named events reset when a checkpoint is created --- cryosparc/api.pyi | 25 +++++++++++++++++++++ cryosparc/controllers/job.py | 43 +++++++++++++++++++++++++++++++----- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/cryosparc/api.pyi b/cryosparc/api.pyi index 1489124b..c1dbbf0c 100644 --- a/cryosparc/api.pyi +++ b/cryosparc/api.pyi @@ -1856,6 +1856,31 @@ class JobsAPI(APINamespace): Returns: CheckpointEvent: Successful Response + """ + ... + def update_event_log( + self, + project_uid: str, + job_uid: str, + event_id: str = "000000000000000000000000", + /, + text: Optional[str] = None, + *, + type: Optional[Literal["text", "warning", "error"]] = None, + ) -> TextEvent: + """ + Update a text event log entry for a job. + + Args: + project_uid (str): Project UID, e.g., "P3" + job_uid (str): Job UID, e.g., "J3" + event_id (str, optional): Defaults to '000000000000000000000000' + text (str, optional): Defaults to None + type (Literal['text', 'warning', 'error'], optional): Defaults to None + + Returns: + TextEvent: Successful Response + """ ... def recalculate_size(self, project_uid: str, job_uid: str, /) -> Job: diff --git a/cryosparc/controllers/job.py b/cryosparc/controllers/job.py index 621204e8..b7ec6495 100644 --- a/cryosparc/controllers/job.py +++ b/cryosparc/controllers/job.py @@ -108,6 +108,13 @@ class JobController(Controller[Job]): Project unique ID, e.g., "P3" """ + _events: dict[str, str] + """ + Named event logs + + :meta private: + """ + def __init__(self, cs: "CryoSPARC", job: Union[Tuple[str, str], Job]) -> None: self.cs = cs if isinstance(job, tuple): @@ -117,6 +124,7 @@ def __init__(self, cs: "CryoSPARC", job: Union[Tuple[str, str], Job]) -> None: self.project_uid = job.project_uid self.uid = job.uid self.model = job + self._events = {} @property def type(self) -> str: @@ -526,24 +534,46 @@ def load_output(self, name: str, slots: LoadableSlots = "all", version: Union[in """ return self.cs.api.jobs.load_output(self.project_uid, self.uid, name, slots=slots, version=version) - def log(self, text: str, level: Literal["text", "warning", "error"] = "text"): + def log(self, text: str, *, level: Literal["text", "warning", "error"] = "text", name: str | None = None): """ - Append to a job's event log. + Append to a job's event log. Update an existing log by providing a name. Args: text (str): Text to log level (str, optional): Log level ("text", "warning" or "error"). Defaults to "text". + name (str, optional): Event name or ID. If an event with the same + name or ID already exists, updates it instead of creating a new + one. Named events are reset when logging a checkpoint. Defaults + to None. + + Example: + + Log a warning message to the job log. + >>> job.log("This is a warning", level="warning") + + Show a live progress bar in the job log. + >>> for pct in range(1, 10): + ... # example log: "Progress: [#####-----] 50%" + ... job.log(f"Progress: [{'#' * pct}{'-' * (10 - pct)}] {pct * 10}%", name="progress") + ... sleep(1) + ... + >>> job.log("Done!") Returns: - str: Created log event ID + str: Created log event name or ID """ - event = self.cs.api.jobs.add_event_log(self.project_uid, self.uid, text, type=level) - return event.id + if name and name in self._events: + event_id = self._events[name] + event = self.cs.api.jobs.update_event_log(self.project_uid, self.uid, event_id, text, type=level) + else: + event = self.cs.api.jobs.add_event_log(self.project_uid, self.uid, text, type=level) + self._events[name or event.id] = event.id + return name or event.id def log_checkpoint(self, meta: dict = {}): """ - Append a checkpoint to the job's event log. + Append a checkpoint to the job's event log. Also resets named events. Args: meta (dict, optional): Additional meta information. Defaults to {}. @@ -552,6 +582,7 @@ def log_checkpoint(self, meta: dict = {}): str: Created checkpoint event ID """ event = self.cs.api.jobs.add_checkpoint(self.project_uid, self.uid, meta) + self._events = {} return event.id def log_plot( From 504049b2b869858f92ad17510d7c28056dab405c Mon Sep 17 00:00:00 2001 From: Nick Frasser <1693461+nfrasser@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:51:04 -0400 Subject: [PATCH 02/10] test(event): tests for updating logs --- tests/controllers/test_job.py | 103 +++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/tests/controllers/test_job.py b/tests/controllers/test_job.py index 1b26d840..2b3132f1 100644 --- a/tests/controllers/test_job.py +++ b/tests/controllers/test_job.py @@ -80,7 +80,10 @@ def test_job_subprocess_io(job: JobController): [sys.executable, "-c", 'import sys; print("hello"); print("error", file=sys.stderr); print("world")'] ) - assert len(mock_log_endpoint.mock_calls) == 7 # includes some prelude/divider calls + # The corrected log method now stores event IDs which causes additional .id property access calls + # Filter to only the actual add_event_log calls, not the .id property accesses + actual_calls = [call for call in mock_log_endpoint.mock_calls if not str(call).endswith(".id.__hash__()")] + assert len(actual_calls) == 7 # includes some prelude/divider calls mock_log_endpoint.assert_has_calls( [ mock.call(job.project_uid, job.uid, "hello", type="text"), @@ -185,3 +188,101 @@ def test_external_job_output(mock_external_job_with_saved_output: ExternalJobCon def test_invalid_external_job_output(external_job): with pytest.raises(ValueError, match="Invalid output name"): external_job.add_output("particle", name="particles/1", slots=["blob", "ctf"]) + + +@pytest.fixture +def mock_log_event(): + return mock.MagicMock(id="event_123") + + +@pytest.fixture +def mock_checkpoint_event(): + return mock.MagicMock(id="checkpoint_456") + + +def test_log_without_name(job: JobController, mock_log_event): + assert isinstance(mock_add_endpoint := APIClient.jobs.add_event_log, mock.Mock) + mock_add_endpoint.return_value = mock_log_event + + result = job.log("Test message without name") + + mock_add_endpoint.assert_called_once_with(job.project_uid, job.uid, "Test message without name", type="text") + assert result == mock_log_event.id + + +def test_log_with_name_create_and_update(job: JobController, mock_log_event): + assert isinstance(mock_add_endpoint := APIClient.jobs.add_event_log, mock.Mock) + assert isinstance(mock_update_endpoint := APIClient.jobs.update_event_log, mock.Mock) + mock_add_endpoint.return_value = mock_log_event + mock_update_endpoint.return_value = mock_log_event + + # First call with name - should create + result1 = job.log("First message", name="progress") + + mock_add_endpoint.assert_called_once_with(job.project_uid, job.uid, "First message", type="text") + assert result1 == "progress" + + # Second call with same name - should update + result2 = job.log("Updated message", level="warning", name="progress") + + mock_update_endpoint.assert_called_once_with( + job.project_uid, job.uid, mock_log_event.id, "Updated message", type="warning" + ) + assert result2 == "progress" + + +def test_log_with_returned_event_id_as_name(job: JobController, mock_log_event): + assert isinstance(mock_add_endpoint := APIClient.jobs.add_event_log, mock.Mock) + assert isinstance(mock_update_endpoint := APIClient.jobs.update_event_log, mock.Mock) + mock_add_endpoint.return_value = mock_log_event + mock_update_endpoint.return_value = mock_log_event + + # First call without name - returns event ID + event_id = job.log("Initial message") + assert event_id == mock_log_event.id + + # Second call using the returned event ID as name - should update + result = job.log("Updated with event ID", name=event_id) + + mock_update_endpoint.assert_called_once_with( + job.project_uid, job.uid, mock_log_event.id, "Updated with event ID", type="text" + ) + assert result == event_id + + +def test_log_after_checkpoint_creates_new(job: JobController, mock_log_event, mock_checkpoint_event): + """Test case 5: log after with a previously-used name immediately after a log_checkpoint (should create)""" + assert isinstance(mock_add_endpoint := APIClient.jobs.add_event_log, mock.Mock) + assert isinstance(mock_update_endpoint := APIClient.jobs.update_event_log, mock.Mock) + assert isinstance(mock_checkpoint_endpoint := APIClient.jobs.add_checkpoint, mock.Mock) + + mock_add_endpoint.return_value = mock_log_event + mock_update_endpoint.return_value = mock_log_event + mock_checkpoint_endpoint.return_value = mock_checkpoint_event + + job.log("Before checkpoint", name="status") + + # Create checkpoint - should clear _events + checkpoint_id = job.log_checkpoint() + mock_checkpoint_endpoint.assert_called_once_with(job.project_uid, job.uid, {}) + assert checkpoint_id == mock_checkpoint_event.id + + # Log again with same name - should create new since _events was cleared + mock_add_endpoint.reset_mock() # Reset to track the second call + result = job.log("After checkpoint", name="status") + mock_add_endpoint.assert_called_with(job.project_uid, job.uid, "After checkpoint", type="text") + assert result == "status" + + +def test_log_with_different_levels(job: JobController, mock_log_event): + """Test logging with different log levels""" + assert isinstance(mock_add_endpoint := APIClient.jobs.add_event_log, mock.Mock) + mock_add_endpoint.return_value = mock_log_event + + # Test warning level + job.log("Warning message", level="warning") + mock_add_endpoint.assert_called_with(job.project_uid, job.uid, "Warning message", type="warning") + + # Test error level + job.log("Error message", level="error") + mock_add_endpoint.assert_called_with(job.project_uid, job.uid, "Error message", type="error") From f43f5132b7187dd26ca652ff1f5341e6c5f4b9b9 Mon Sep 17 00:00:00 2001 From: Nick Frasser <1693461+nfrasser@users.noreply.github.com> Date: Fri, 19 Sep 2025 11:38:51 -0400 Subject: [PATCH 03/10] test(event): cleanup --- tests/controllers/test_job.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/controllers/test_job.py b/tests/controllers/test_job.py index 2b3132f1..b74f49b4 100644 --- a/tests/controllers/test_job.py +++ b/tests/controllers/test_job.py @@ -251,7 +251,6 @@ def test_log_with_returned_event_id_as_name(job: JobController, mock_log_event): def test_log_after_checkpoint_creates_new(job: JobController, mock_log_event, mock_checkpoint_event): - """Test case 5: log after with a previously-used name immediately after a log_checkpoint (should create)""" assert isinstance(mock_add_endpoint := APIClient.jobs.add_event_log, mock.Mock) assert isinstance(mock_update_endpoint := APIClient.jobs.update_event_log, mock.Mock) assert isinstance(mock_checkpoint_endpoint := APIClient.jobs.add_checkpoint, mock.Mock) @@ -262,27 +261,23 @@ def test_log_after_checkpoint_creates_new(job: JobController, mock_log_event, mo job.log("Before checkpoint", name="status") - # Create checkpoint - should clear _events checkpoint_id = job.log_checkpoint() mock_checkpoint_endpoint.assert_called_once_with(job.project_uid, job.uid, {}) assert checkpoint_id == mock_checkpoint_event.id - # Log again with same name - should create new since _events was cleared + # Log again with same name - should create new mock_add_endpoint.reset_mock() # Reset to track the second call result = job.log("After checkpoint", name="status") - mock_add_endpoint.assert_called_with(job.project_uid, job.uid, "After checkpoint", type="text") + mock_add_endpoint.assert_called_once_with(job.project_uid, job.uid, "After checkpoint", type="text") assert result == "status" def test_log_with_different_levels(job: JobController, mock_log_event): - """Test logging with different log levels""" assert isinstance(mock_add_endpoint := APIClient.jobs.add_event_log, mock.Mock) mock_add_endpoint.return_value = mock_log_event - # Test warning level job.log("Warning message", level="warning") mock_add_endpoint.assert_called_with(job.project_uid, job.uid, "Warning message", type="warning") - # Test error level job.log("Error message", level="error") mock_add_endpoint.assert_called_with(job.project_uid, job.uid, "Error message", type="error") From 9f371d63a278acb44468121aa6cd4f5eab03c60b Mon Sep 17 00:00:00 2001 From: Nick Frasser <1693461+nfrasser@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:20:01 -0400 Subject: [PATCH 04/10] fix(event): typo for python 3.8 --- cryosparc/controllers/job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryosparc/controllers/job.py b/cryosparc/controllers/job.py index b7ec6495..4bc6e17e 100644 --- a/cryosparc/controllers/job.py +++ b/cryosparc/controllers/job.py @@ -108,7 +108,7 @@ class JobController(Controller[Job]): Project unique ID, e.g., "P3" """ - _events: dict[str, str] + _events: Dict[str, str] """ Named event logs From f851df3bb48c107cdb68ed2af09bc1723532db03 Mon Sep 17 00:00:00 2001 From: Nick Frasser <1693461+nfrasser@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:44:21 -0400 Subject: [PATCH 05/10] fix(event): typo for python 3.8 --- cryosparc/controllers/job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryosparc/controllers/job.py b/cryosparc/controllers/job.py index 4bc6e17e..09f75e66 100644 --- a/cryosparc/controllers/job.py +++ b/cryosparc/controllers/job.py @@ -534,7 +534,7 @@ def load_output(self, name: str, slots: LoadableSlots = "all", version: Union[in """ return self.cs.api.jobs.load_output(self.project_uid, self.uid, name, slots=slots, version=version) - def log(self, text: str, *, level: Literal["text", "warning", "error"] = "text", name: str | None = None): + def log(self, text: str, *, level: Literal["text", "warning", "error"] = "text", name: Optional[str] = None): """ Append to a job's event log. Update an existing log by providing a name. From 8cd6feb62d1d9883b91ac1488f69c30a5bc559f4 Mon Sep 17 00:00:00 2001 From: Nick Frasser <1693461+nfrasser@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:35:05 -0400 Subject: [PATCH 06/10] fix(event): review fixes --- cryosparc/controllers/job.py | 8 ++++++-- tests/controllers/test_job.py | 15 --------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/cryosparc/controllers/job.py b/cryosparc/controllers/job.py index 09f75e66..f47a8e68 100644 --- a/cryosparc/controllers/job.py +++ b/cryosparc/controllers/job.py @@ -110,7 +110,9 @@ class JobController(Controller[Job]): _events: Dict[str, str] """ - Named event logs + Named event logs. Key can be user-provided name in log() method, or ID if + name not provided. If both name and ID are used, can have two keys with + the same value. :meta private: """ @@ -568,7 +570,9 @@ def log(self, text: str, *, level: Literal["text", "warning", "error"] = "text", event = self.cs.api.jobs.update_event_log(self.project_uid, self.uid, event_id, text, type=level) else: event = self.cs.api.jobs.add_event_log(self.project_uid, self.uid, text, type=level) - self._events[name or event.id] = event.id + self._events[event.id] = event.id + if name: + self._events[name] = event.id return name or event.id def log_checkpoint(self, meta: dict = {}): diff --git a/tests/controllers/test_job.py b/tests/controllers/test_job.py index b74f49b4..c0bba137 100644 --- a/tests/controllers/test_job.py +++ b/tests/controllers/test_job.py @@ -80,10 +80,6 @@ def test_job_subprocess_io(job: JobController): [sys.executable, "-c", 'import sys; print("hello"); print("error", file=sys.stderr); print("world")'] ) - # The corrected log method now stores event IDs which causes additional .id property access calls - # Filter to only the actual add_event_log calls, not the .id property accesses - actual_calls = [call for call in mock_log_endpoint.mock_calls if not str(call).endswith(".id.__hash__()")] - assert len(actual_calls) == 7 # includes some prelude/divider calls mock_log_endpoint.assert_has_calls( [ mock.call(job.project_uid, job.uid, "hello", type="text"), @@ -270,14 +266,3 @@ def test_log_after_checkpoint_creates_new(job: JobController, mock_log_event, mo result = job.log("After checkpoint", name="status") mock_add_endpoint.assert_called_once_with(job.project_uid, job.uid, "After checkpoint", type="text") assert result == "status" - - -def test_log_with_different_levels(job: JobController, mock_log_event): - assert isinstance(mock_add_endpoint := APIClient.jobs.add_event_log, mock.Mock) - mock_add_endpoint.return_value = mock_log_event - - job.log("Warning message", level="warning") - mock_add_endpoint.assert_called_with(job.project_uid, job.uid, "Warning message", type="warning") - - job.log("Error message", level="error") - mock_add_endpoint.assert_called_with(job.project_uid, job.uid, "Error message", type="error") From 51a22fd367c2b64145f94fa454ca80c6bc27c432 Mon Sep 17 00:00:00 2001 From: Nick Frasser <1693461+nfrasser@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:42:12 -0400 Subject: [PATCH 07/10] fix(event): revert to name/id behaviour --- cryosparc/controllers/job.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cryosparc/controllers/job.py b/cryosparc/controllers/job.py index f47a8e68..4299217b 100644 --- a/cryosparc/controllers/job.py +++ b/cryosparc/controllers/job.py @@ -570,9 +570,7 @@ def log(self, text: str, *, level: Literal["text", "warning", "error"] = "text", event = self.cs.api.jobs.update_event_log(self.project_uid, self.uid, event_id, text, type=level) else: event = self.cs.api.jobs.add_event_log(self.project_uid, self.uid, text, type=level) - self._events[event.id] = event.id - if name: - self._events[name] = event.id + self._events[name or event.id] = event.id return name or event.id def log_checkpoint(self, meta: dict = {}): From 4aea1680631b824af1014e0e92365c236518781b Mon Sep 17 00:00:00 2001 From: Nick Frasser <1693461+nfrasser@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:42:40 -0400 Subject: [PATCH 08/10] docs(event): better name docstring --- cryosparc/controllers/job.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cryosparc/controllers/job.py b/cryosparc/controllers/job.py index 4299217b..98f1ddfe 100644 --- a/cryosparc/controllers/job.py +++ b/cryosparc/controllers/job.py @@ -544,10 +544,11 @@ def log(self, text: str, *, level: Literal["text", "warning", "error"] = "text", text (str): Text to log level (str, optional): Log level ("text", "warning" or "error"). Defaults to "text". - name (str, optional): Event name or ID. If an event with the same - name or ID already exists, updates it instead of creating a new - one. Named events are reset when logging a checkpoint. Defaults - to None. + name (str, optional): Event name or ID. If called multiple times + with the same name, updates that event instead of creating a new + one. If name is not initially provided, can pass returned ID as + name. Named events are reset when logging a checkpoint. + Defaults to None. Example: @@ -562,6 +563,11 @@ def log(self, text: str, *, level: Literal["text", "warning", "error"] = "text", ... >>> job.log("Done!") + Update an existing log event by ID. + >>> event_id = job.log("Starting job processing...") + >>> # do some processing... + >>> job.log("Finished processing", name=event_id) + Returns: str: Created log event name or ID """ From 2343312903955c733a1986ffd1075b08eeadc96a Mon Sep 17 00:00:00 2001 From: Nick Frasser <1693461+nfrasser@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:13:24 -0400 Subject: [PATCH 09/10] refactor(event): separate name and ID arguments No longer use ID as name to reduce ambiguity --- cryosparc/controllers/job.py | 42 +++++++++++++++++++++++++---------- tests/controllers/test_job.py | 20 ++++++++--------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/cryosparc/controllers/job.py b/cryosparc/controllers/job.py index 98f1ddfe..10114d0a 100644 --- a/cryosparc/controllers/job.py +++ b/cryosparc/controllers/job.py @@ -57,6 +57,11 @@ Input and output result groups may only contain, letters, numbers and underscores. """ +LogLevel = Literal["text", "warning", "error"] +""" +Severity level for job event logs. +""" + class JobController(Controller[Job]): """ @@ -536,19 +541,27 @@ def load_output(self, name: str, slots: LoadableSlots = "all", version: Union[in """ return self.cs.api.jobs.load_output(self.project_uid, self.uid, name, slots=slots, version=version) - def log(self, text: str, *, level: Literal["text", "warning", "error"] = "text", name: Optional[str] = None): + @overload + def log(self, text: str, *, level: LogLevel = ...) -> str: ... + @overload + def log(self, text: str, *, level: LogLevel = ..., name: str) -> str: ... + @overload + def log(self, text: str, *, level: LogLevel = ..., id: str) -> str: ... + def log(self, text: str, *, level: LogLevel = "text", name: Optional[str] = None, id: Optional[str] = None) -> str: """ - Append to a job's event log. Update an existing log by providing a name. + Append to a job's event log. Update an existing log by providing a name + or ID. Args: text (str): Text to log level (str, optional): Log level ("text", "warning" or "error"). Defaults to "text". - name (str, optional): Event name or ID. If called multiple times - with the same name, updates that event instead of creating a new - one. If name is not initially provided, can pass returned ID as - name. Named events are reset when logging a checkpoint. - Defaults to None. + name (str, optional): Event name. If called multiple times with the + same name, updates that event instead of creating a new one. + Named events are reset when logging a checkpoint. Cannot be + provided with id. Defaults to None. + id (str, optional): Update a previously-created event log by its ID. + Cannot be provided with name. Defaults to None. Example: @@ -569,15 +582,20 @@ def log(self, text: str, *, level: Literal["text", "warning", "error"] = "text", >>> job.log("Finished processing", name=event_id) Returns: - str: Created log event name or ID + str: Created log event ID """ + existing_id = id if name and name in self._events: - event_id = self._events[name] - event = self.cs.api.jobs.update_event_log(self.project_uid, self.uid, event_id, text, type=level) + existing_id = self._events[name] + + if existing_id: + event = self.cs.api.jobs.update_event_log(self.project_uid, self.uid, existing_id, text, type=level) else: event = self.cs.api.jobs.add_event_log(self.project_uid, self.uid, text, type=level) - self._events[name or event.id] = event.id - return name or event.id + + if name: + self._events[name] = event.id + return event.id def log_checkpoint(self, meta: dict = {}): """ diff --git a/tests/controllers/test_job.py b/tests/controllers/test_job.py index c0bba137..8d8c34fe 100644 --- a/tests/controllers/test_job.py +++ b/tests/controllers/test_job.py @@ -196,7 +196,7 @@ def mock_checkpoint_event(): return mock.MagicMock(id="checkpoint_456") -def test_log_without_name(job: JobController, mock_log_event): +def test_log(job: JobController, mock_log_event): assert isinstance(mock_add_endpoint := APIClient.jobs.add_event_log, mock.Mock) mock_add_endpoint.return_value = mock_log_event @@ -213,32 +213,32 @@ def test_log_with_name_create_and_update(job: JobController, mock_log_event): mock_update_endpoint.return_value = mock_log_event # First call with name - should create - result1 = job.log("First message", name="progress") + event_id = job.log("First message", name="progress") mock_add_endpoint.assert_called_once_with(job.project_uid, job.uid, "First message", type="text") - assert result1 == "progress" + assert event_id == "event_123" # Second call with same name - should update - result2 = job.log("Updated message", level="warning", name="progress") + event_id = job.log("Updated message", level="warning", name="progress") mock_update_endpoint.assert_called_once_with( job.project_uid, job.uid, mock_log_event.id, "Updated message", type="warning" ) - assert result2 == "progress" + assert event_id == "event_123" -def test_log_with_returned_event_id_as_name(job: JobController, mock_log_event): +def test_log_with_returned_event_id(job: JobController, mock_log_event): assert isinstance(mock_add_endpoint := APIClient.jobs.add_event_log, mock.Mock) assert isinstance(mock_update_endpoint := APIClient.jobs.update_event_log, mock.Mock) mock_add_endpoint.return_value = mock_log_event mock_update_endpoint.return_value = mock_log_event - # First call without name - returns event ID + # First call without ID - returns event ID event_id = job.log("Initial message") assert event_id == mock_log_event.id - # Second call using the returned event ID as name - should update - result = job.log("Updated with event ID", name=event_id) + # Second call using the returned event ID - should update + result = job.log("Updated with event ID", id=event_id) mock_update_endpoint.assert_called_once_with( job.project_uid, job.uid, mock_log_event.id, "Updated with event ID", type="text" @@ -265,4 +265,4 @@ def test_log_after_checkpoint_creates_new(job: JobController, mock_log_event, mo mock_add_endpoint.reset_mock() # Reset to track the second call result = job.log("After checkpoint", name="status") mock_add_endpoint.assert_called_once_with(job.project_uid, job.uid, "After checkpoint", type="text") - assert result == "status" + assert result == "event_123" From efb28cdb2de548ad0f5ac8bb3c10abd261710c30 Mon Sep 17 00:00:00 2001 From: Nick Frasser <1693461+nfrasser@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:33:17 -0400 Subject: [PATCH 10/10] docs(event): fix docstring typo --- cryosparc/controllers/job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryosparc/controllers/job.py b/cryosparc/controllers/job.py index 10114d0a..f8ddaafd 100644 --- a/cryosparc/controllers/job.py +++ b/cryosparc/controllers/job.py @@ -579,7 +579,7 @@ def log(self, text: str, *, level: LogLevel = "text", name: Optional[str] = None Update an existing log event by ID. >>> event_id = job.log("Starting job processing...") >>> # do some processing... - >>> job.log("Finished processing", name=event_id) + >>> job.log("Finished processing", id=event_id) Returns: str: Created log event ID