From 56240c035cc0b497e331bc5349b9791e01ee5e34 Mon Sep 17 00:00:00 2001 From: Victor Morand Date: Fri, 31 Jul 2026 16:35:59 +0200 Subject: [PATCH] fix: 'dict' object has no attribute 'key' error --- scripts/make-app-icon.py | 1 - src/experimaestro/scheduler/remote/client.py | 13 +++++++-- src/experimaestro/scheduler/state_provider.py | 15 +++++++++- src/experimaestro/tests/test_remote_state.py | 29 +++++++++++++++++++ src/experimaestro/tui/app.py | 24 +++++++++++---- src/experimaestro/webui/state_bridge.py | 8 ++++- 6 files changed, 78 insertions(+), 12 deletions(-) diff --git a/scripts/make-app-icon.py b/scripts/make-app-icon.py index d93a682c..9f632406 100644 --- a/scripts/make-app-icon.py +++ b/scripts/make-app-icon.py @@ -43,7 +43,6 @@ from __future__ import annotations import argparse -import colorsys import re import subprocess import sys diff --git a/src/experimaestro/scheduler/remote/client.py b/src/experimaestro/scheduler/remote/client.py index d0c26c8e..26732ab8 100644 --- a/src/experimaestro/scheduler/remote/client.py +++ b/src/experimaestro/scheduler/remote/client.py @@ -876,15 +876,22 @@ def _notification_to_event(self, method: str, params: Dict) -> Optional[EventBas event_type = params.get("event_type") data = params.get("data", {}) + if not event_type: + logger.warning("Notification missing event_type: %s", params) + return None + event_class = EventBase.get_class(event_type) if event_class is None: logger.warning("Unknown event type: %s", event_type) return None + event_dict = dict(data) + event_dict["event_type"] = event_type + try: - return event_class(**data) - except TypeError as e: - logger.warning("Error deserializing event %s: %s", event_type, e) + return EventBase.from_dict(event_dict) + except Exception as e: + logger.warning("Error deserializing event %s: %s", event_type, e, exc_info=True) return None def _notify_listeners(self, event: EventBase): diff --git a/src/experimaestro/scheduler/state_provider.py b/src/experimaestro/scheduler/state_provider.py index 79c90ea5..94a47d51 100644 --- a/src/experimaestro/scheduler/state_provider.py +++ b/src/experimaestro/scheduler/state_provider.py @@ -1243,7 +1243,20 @@ def _add_submitted_job(self, event: "EventBase") -> None: self._job_cache[cache_key] = job logger.debug("Added job %s to cache from event", cache_key) - tags = {tag.key: tag.value for tag in getattr(event, "tags", [])} + tags = {} + for tag in getattr(event, "tags", []) or []: + if isinstance(tag, dict): + k, v = tag.get("key"), tag.get("value") + else: + k, v = getattr(tag, "key", None), getattr(tag, "value", None) + if k is not None: + tags[k] = v + else: + logger.warning( + "Invalid tag element in event %s: %s", + type(event).__name__, + tag, + ) depends_on = list(getattr(event, "depends_on", [])) job_info = ExperimentJobInformation( job_id=event.job_id, diff --git a/src/experimaestro/tests/test_remote_state.py b/src/experimaestro/tests/test_remote_state.py index ee5386ac..2796f95a 100644 --- a/src/experimaestro/tests/test_remote_state.py +++ b/src/experimaestro/tests/test_remote_state.py @@ -1753,6 +1753,35 @@ def test_client_notification_non_state_event(self, client): assert event is None + def test_client_notification_to_event_job_submitted_tags(self, client): + """Test client converts notification to JobSubmittedEvent with JobTag objects""" + from experimaestro.scheduler.state_status import JobSubmittedEvent, JobTag + from experimaestro.scheduler.remote.protocol import NotificationMethod + + event = client._notification_to_event( + NotificationMethod.STATE_EVENT.value, + { + "event_type": "JobSubmittedEvent", + "data": { + "job_id": "job_tags_test", + "task_id": "task123", + "run_id": "run_001", + "tags": [{"key": "model", "value": "bert"}, {"key": "lr", "value": "0.01"}], + "timestamp": 1704067260.0, + }, + }, + ) + + assert isinstance(event, JobSubmittedEvent) + assert len(event.tags) == 2 + assert isinstance(event.tags[0], JobTag) + assert event.tags[0].key == "model" + assert event.tags[0].value == "bert" + + # Verify _add_submitted_job handles the event without AttributeError + client._add_submitted_job(event) + + # ============================================================================= # Error Handling Tests diff --git a/src/experimaestro/tui/app.py b/src/experimaestro/tui/app.py index 44405142..ddfa475c 100644 --- a/src/experimaestro/tui/app.py +++ b/src/experimaestro/tui/app.py @@ -489,9 +489,23 @@ def _handle_job_submitted(self, event: JobSubmittedEvent) -> None: if jobs_table.current_experiment == event_exp_id: # Add the new job's tags to the cache if event.tags: - jobs_table.tags_map[event.job_id] = { - tag.key: tag.value for tag in event.tags - } + event_tags = {} + for tag in event.tags: + k = ( + tag.get("key") + if isinstance(tag, dict) + else getattr(tag, "key", None) + ) + v = ( + tag.get("value") + if isinstance(tag, dict) + else getattr(tag, "value", None) + ) + if k is not None: + event_tags[k] = v + jobs_table.tags_map[event.job_id] = event_tags + else: + event_tags = {} # Add the new job's dependencies to the cache if event.depends_on: jobs_table.dependencies_map[event.job_id] = event.depends_on @@ -508,9 +522,7 @@ def _handle_job_submitted(self, event: JobSubmittedEvent) -> None: jobs_table.experiment_job_info[event.job_id] = ExperimentJobInformation( job_id=event.job_id, task_id=event.task_id, - tags=( - {tag.key: tag.value for tag in event.tags} if event.tags else {} - ), + tags=event_tags, timestamp=timestamp, ) # Refresh to show the new job diff --git a/src/experimaestro/webui/state_bridge.py b/src/experimaestro/webui/state_bridge.py index f1ca0d4c..8a788c87 100644 --- a/src/experimaestro/webui/state_bridge.py +++ b/src/experimaestro/webui/state_bridge.py @@ -162,7 +162,13 @@ async def _handle_job_submitted(self, event: JobSubmittedEvent): # Fetch the full job data from state provider job = self.state_provider.get_job(event.task_id, event.job_id) if job: - tags = [(tag.key, tag.value) for tag in event.tags] if event.tags else [] + tags = [ + ( + tag["key"] if isinstance(tag, dict) else getattr(tag, "key"), + tag["value"] if isinstance(tag, dict) else getattr(tag, "value"), + ) + for tag in event.tags + ] if event.tags else [] payload = serialize_job( job, tags=tags,