diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 0ec19d90..d3f35913 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -26,6 +26,8 @@ jobs: run: uv sync --dev - name: Run Ruff Linter run: uv run ruff check . + - name: Run Ruff Format Check + run: uv run ruff format --check . - name: Run MyPy Type Checker run: uv run mypy src - name: Run Pyright (Pylance equivalent) diff --git a/src/a2a/server/request_handlers/jsonrpc_handler.py b/src/a2a/server/request_handlers/jsonrpc_handler.py index 3e79bc09..5de7dae9 100644 --- a/src/a2a/server/request_handlers/jsonrpc_handler.py +++ b/src/a2a/server/request_handlers/jsonrpc_handler.py @@ -347,10 +347,8 @@ async def list_push_notification_config( A `ListTaskPushNotificationConfigResponse` object containing the config or a JSON-RPC error. """ try: - config = ( - await self.request_handler.on_list_task_push_notification_config( - request.params, context - ) + config = await self.request_handler.on_list_task_push_notification_config( + request.params, context ) return prepare_response_object( request.id, diff --git a/src/a2a/server/request_handlers/response_helpers.py b/src/a2a/server/request_handlers/response_helpers.py index 7f005099..4c55c419 100644 --- a/src/a2a/server/request_handlers/response_helpers.py +++ b/src/a2a/server/request_handlers/response_helpers.py @@ -41,7 +41,7 @@ GetTaskPushNotificationConfigResponse, SendStreamingMessageResponse, ListTaskPushNotificationConfigResponse, - DeleteTaskPushNotificationConfigResponse + DeleteTaskPushNotificationConfigResponse, ) """Type variable for RootModel response types.""" @@ -55,7 +55,7 @@ GetTaskPushNotificationConfigSuccessResponse, SendStreamingMessageSuccessResponse, ListTaskPushNotificationConfigSuccessResponse, - DeleteTaskPushNotificationConfigSuccessResponse + DeleteTaskPushNotificationConfigSuccessResponse, ) """Type variable for SuccessResponse types.""" diff --git a/src/a2a/server/tasks/push_notification_config_store.py b/src/a2a/server/tasks/push_notification_config_store.py index dd93791f..efe46b40 100644 --- a/src/a2a/server/tasks/push_notification_config_store.py +++ b/src/a2a/server/tasks/push_notification_config_store.py @@ -7,7 +7,9 @@ class PushNotificationConfigStore(ABC): """Interface for storing and retrieving push notification configurations for tasks.""" @abstractmethod - async def set_info(self, task_id: str, notification_config: PushNotificationConfig) -> None: + async def set_info( + self, task_id: str, notification_config: PushNotificationConfig + ) -> None: """Sets or updates the push notification configuration for a task.""" @abstractmethod @@ -15,5 +17,7 @@ async def get_info(self, task_id: str) -> list[PushNotificationConfig]: """Retrieves the push notification configuration for a task.""" @abstractmethod - async def delete_info(self, task_id: str, config_id: str | None = None) -> None: + async def delete_info( + self, task_id: str, config_id: str | None = None + ) -> None: """Deletes the push notification configuration for a task.""" diff --git a/src/a2a/server/tasks/task_updater.py b/src/a2a/server/tasks/task_updater.py index d8fa21af..11157456 100644 --- a/src/a2a/server/tasks/task_updater.py +++ b/src/a2a/server/tasks/task_updater.py @@ -60,12 +60,18 @@ async def update_status( """ async with self._lock: if self._terminal_state_reached: - raise RuntimeError(f"Task {self.task_id} is already in a terminal state.") + raise RuntimeError( + f'Task {self.task_id} is already in a terminal state.' + ) if state in self._terminal_states: self._terminal_state_reached = True final = True - current_timestamp = timestamp if timestamp else datetime.now(timezone.utc).isoformat() + current_timestamp = ( + timestamp + if timestamp + else datetime.now(timezone.utc).isoformat() + ) await self.event_queue.enqueue_event( TaskStatusUpdateEvent( taskId=self.task_id, @@ -112,7 +118,7 @@ async def add_artifact( # noqa: PLR0913 metadata=metadata, ), append=append, - lastChunk=last_chunk + lastChunk=last_chunk, ) ) diff --git a/tests/server/request_handlers/test_jsonrpc_handler.py b/tests/server/request_handlers/test_jsonrpc_handler.py index a4400ead..1f421aef 100644 --- a/tests/server/request_handlers/test_jsonrpc_handler.py +++ b/tests/server/request_handlers/test_jsonrpc_handler.py @@ -16,7 +16,13 @@ from a2a.server.events import QueueManager from a2a.server.events.event_queue import EventQueue from a2a.server.request_handlers import DefaultRequestHandler, JSONRPCHandler -from a2a.server.tasks import TaskStore, InMemoryPushNotificationConfigStore, BasePushNotificationSender, PushNotificationConfigStore, PushNotificationSender +from a2a.server.tasks import ( + TaskStore, + InMemoryPushNotificationConfigStore, + BasePushNotificationSender, + PushNotificationConfigStore, + PushNotificationSender, +) from a2a.types import ( AgentCapabilities, AgentCard, @@ -436,8 +442,10 @@ async def streaming_coro(): async def test_set_push_notification_success(self) -> None: mock_agent_executor = AsyncMock(spec=AgentExecutor) mock_task_store = AsyncMock(spec=TaskStore) - mock_push_notification_store = AsyncMock(spec=PushNotificationConfigStore) - + mock_push_notification_store = AsyncMock( + spec=PushNotificationConfigStore + ) + request_handler = DefaultRequestHandler( mock_agent_executor, mock_task_store, @@ -471,10 +479,12 @@ async def test_set_push_notification_success(self) -> None: async def test_get_push_notification_success(self) -> None: mock_agent_executor = AsyncMock(spec=AgentExecutor) - mock_task_store = AsyncMock(spec=TaskStore) + mock_task_store = AsyncMock(spec=TaskStore) push_notification_store = InMemoryPushNotificationConfigStore() request_handler = DefaultRequestHandler( - mock_agent_executor, mock_task_store, push_config_store=push_notification_store + mock_agent_executor, + mock_task_store, + push_config_store=push_notification_store, ) self.mock_agent_card.capabilities = AgentCapabilities( streaming=True, pushNotifications=True @@ -516,9 +526,14 @@ async def test_on_message_stream_new_message_send_push_notification_success( mock_task_store = AsyncMock(spec=TaskStore) mock_httpx_client = AsyncMock(spec=httpx.AsyncClient) push_notification_store = InMemoryPushNotificationConfigStore() - push_notification_sender = BasePushNotificationSender(mock_httpx_client, push_notification_store) + push_notification_sender = BasePushNotificationSender( + mock_httpx_client, push_notification_store + ) request_handler = DefaultRequestHandler( - mock_agent_executor, mock_task_store, push_config_store=push_notification_store, push_sender=push_notification_sender + mock_agent_executor, + mock_task_store, + push_config_store=push_notification_store, + push_sender=push_notification_sender, ) self.mock_agent_card.capabilities = AgentCapabilities( streaming=True, pushNotifications=True @@ -585,7 +600,7 @@ async def streaming_coro(): 'kind': 'task', 'status': {'state': 'submitted'}, }, - headers=None + headers=None, ), call( 'http://example.com', @@ -606,7 +621,7 @@ async def streaming_coro(): 'kind': 'task', 'status': {'state': 'submitted'}, }, - headers=None + headers=None, ), call( 'http://example.com', @@ -627,7 +642,7 @@ async def streaming_coro(): 'kind': 'task', 'status': {'state': 'completed'}, }, - headers=None + headers=None, ), ] mock_httpx_client.post.assert_has_calls(calls) @@ -727,7 +742,7 @@ async def test_streaming_not_supported_error( pass self.assertEqual( - str(context.exception.error.message), # type: ignore + str(context.exception.error.message), # type: ignore 'Streaming is not supported by the agent', ) @@ -761,7 +776,7 @@ async def test_push_notifications_not_supported_error(self) -> None: await handler.set_push_notification_config(request) self.assertEqual( - str(context.exception.error.message), # type: ignore + str(context.exception.error.message), # type: ignore 'Push notifications are not supported by the agent', ) @@ -960,7 +975,7 @@ async def consume_raises_error(*args, **kwargs) -> NoReturn: # Assert self.assertIsInstance(response.root, JSONRPCErrorResponse) - self.assertEqual(response.root.error, UnsupportedOperationError()) # type: ignore + self.assertEqual(response.root.error, UnsupportedOperationError()) # type: ignore async def test_on_message_send_task_id_mismatch(self) -> None: mock_agent_executor = AsyncMock(spec=AgentExecutor) @@ -1031,24 +1046,35 @@ async def test_on_get_push_notification(self) -> None: mock_task = Task(**MINIMAL_TASK) mock_task_store.get.return_value = mock_task - # Create request handler without a push notifier request_handler = AsyncMock(spec=DefaultRequestHandler) - task_push_config = TaskPushNotificationConfig(taskId=mock_task.id, pushNotificationConfig=PushNotificationConfig(id="config1", url='http://example.com')) - request_handler.on_get_task_push_notification_config.return_value = task_push_config + task_push_config = TaskPushNotificationConfig( + taskId=mock_task.id, + pushNotificationConfig=PushNotificationConfig( + id='config1', url='http://example.com' + ), + ) + request_handler.on_get_task_push_notification_config.return_value = ( + task_push_config + ) self.mock_agent_card.capabilities = AgentCapabilities( pushNotifications=True ) handler = JSONRPCHandler(self.mock_agent_card, request_handler) list_request = GetTaskPushNotificationConfigRequest( - id='1', params=GetTaskPushNotificationConfigParams(id=mock_task.id, pushNotificationConfigId="config1") + id='1', + params=GetTaskPushNotificationConfigParams( + id=mock_task.id, pushNotificationConfigId='config1' + ), ) response = await handler.get_push_notification_config(list_request) # Assert - self.assertIsInstance(response.root, GetTaskPushNotificationConfigSuccessResponse) - self.assertEqual(response.root.result, task_push_config) # type: ignore + self.assertIsInstance( + response.root, GetTaskPushNotificationConfigSuccessResponse + ) + self.assertEqual(response.root.result, task_push_config) # type: ignore async def test_on_list_push_notification(self) -> None: """Test list_push_notification_config handling""" @@ -1056,12 +1082,18 @@ async def test_on_list_push_notification(self) -> None: mock_task = Task(**MINIMAL_TASK) mock_task_store.get.return_value = mock_task - # Create request handler without a push notifier request_handler = AsyncMock(spec=DefaultRequestHandler) - task_push_config = TaskPushNotificationConfig(taskId=mock_task.id, pushNotificationConfig=PushNotificationConfig(url='http://example.com')) - request_handler.on_list_task_push_notification_config.return_value = [task_push_config] + task_push_config = TaskPushNotificationConfig( + taskId=mock_task.id, + pushNotificationConfig=PushNotificationConfig( + url='http://example.com' + ), + ) + request_handler.on_list_task_push_notification_config.return_value = [ + task_push_config + ] self.mock_agent_card.capabilities = AgentCapabilities( pushNotifications=True @@ -1072,8 +1104,10 @@ async def test_on_list_push_notification(self) -> None: ) response = await handler.list_push_notification_config(list_request) # Assert - self.assertIsInstance(response.root, ListTaskPushNotificationConfigSuccessResponse) - self.assertEqual(response.root.result, [task_push_config]) # type: ignore + self.assertIsInstance( + response.root, ListTaskPushNotificationConfigSuccessResponse + ) + self.assertEqual(response.root.result, [task_push_config]) # type: ignore async def test_on_list_push_notification_error(self) -> None: """Test list_push_notification_config handling""" @@ -1081,14 +1115,19 @@ async def test_on_list_push_notification_error(self) -> None: mock_task = Task(**MINIMAL_TASK) mock_task_store.get.return_value = mock_task - # Create request handler without a push notifier request_handler = AsyncMock(spec=DefaultRequestHandler) - task_push_config = TaskPushNotificationConfig(taskId=mock_task.id, pushNotificationConfig=PushNotificationConfig(url='http://example.com')) + task_push_config = TaskPushNotificationConfig( + taskId=mock_task.id, + pushNotificationConfig=PushNotificationConfig( + url='http://example.com' + ), + ) # throw server error - request_handler.on_list_task_push_notification_config.side_effect = ServerError(InternalError()) - + request_handler.on_list_task_push_notification_config.side_effect = ( + ServerError(InternalError()) + ) self.mock_agent_card.capabilities = AgentCapabilities( pushNotifications=True @@ -1100,45 +1139,55 @@ async def test_on_list_push_notification_error(self) -> None: response = await handler.list_push_notification_config(list_request) # Assert self.assertIsInstance(response.root, JSONRPCErrorResponse) - self.assertEqual(response.root.error, InternalError()) # type: ignore - + self.assertEqual(response.root.error, InternalError()) # type: ignore + async def test_on_delete_push_notification(self) -> None: """Test delete_push_notification_config handling""" # Create request handler without a push notifier - request_handler = AsyncMock(spec=DefaultRequestHandler) - request_handler.on_delete_task_push_notification_config.return_value = None + request_handler = AsyncMock(spec=DefaultRequestHandler) + request_handler.on_delete_task_push_notification_config.return_value = ( + None + ) self.mock_agent_card.capabilities = AgentCapabilities( pushNotifications=True ) handler = JSONRPCHandler(self.mock_agent_card, request_handler) delete_request = DeleteTaskPushNotificationConfigRequest( - id='1', params=DeleteTaskPushNotificationConfigParams(id="task1", pushNotificationConfigId="config1") + id='1', + params=DeleteTaskPushNotificationConfigParams( + id='task1', pushNotificationConfigId='config1' + ), ) response = await handler.delete_push_notification_config(delete_request) # Assert - self.assertIsInstance(response.root, DeleteTaskPushNotificationConfigSuccessResponse) - self.assertEqual(response.root.result, None) # type: ignore + self.assertIsInstance( + response.root, DeleteTaskPushNotificationConfigSuccessResponse + ) + self.assertEqual(response.root.result, None) # type: ignore async def test_on_delete_push_notification_error(self) -> None: """Test delete_push_notification_config error handling""" - # Create request handler without a push notifier request_handler = AsyncMock(spec=DefaultRequestHandler) # throw server error - request_handler.on_delete_task_push_notification_config.side_effect = ServerError(UnsupportedOperationError()) - + request_handler.on_delete_task_push_notification_config.side_effect = ( + ServerError(UnsupportedOperationError()) + ) self.mock_agent_card.capabilities = AgentCapabilities( pushNotifications=True ) handler = JSONRPCHandler(self.mock_agent_card, request_handler) delete_request = DeleteTaskPushNotificationConfigRequest( - id='1', params=DeleteTaskPushNotificationConfigParams(id="task1", pushNotificationConfigId="config1") + id='1', + params=DeleteTaskPushNotificationConfigParams( + id='task1', pushNotificationConfigId='config1' + ), ) response = await handler.delete_push_notification_config(delete_request) # Assert self.assertIsInstance(response.root, JSONRPCErrorResponse) - self.assertEqual(response.root.error, UnsupportedOperationError()) # type: ignore \ No newline at end of file + self.assertEqual(response.root.error, UnsupportedOperationError()) # type: ignore diff --git a/tests/server/tasks/test_inmemory_push_notifications.py b/tests/server/tasks/test_inmemory_push_notifications.py index 9fe4eee7..7913952e 100644 --- a/tests/server/tasks/test_inmemory_push_notifications.py +++ b/tests/server/tasks/test_inmemory_push_notifications.py @@ -161,7 +161,9 @@ async def test_send_notification_success(self): async def test_send_notification_with_token_success(self): task_id = 'task_send_success' task_data = create_sample_task(task_id=task_id) - config = create_sample_push_config(url='http://notify.me/here', token='unique_token') + config = create_sample_push_config( + url='http://notify.me/here', token='unique_token' + ) await self.config_store.set_info(task_id, config) # Mock the post call to simulate success @@ -180,7 +182,7 @@ async def test_send_notification_with_token_success(self): ) self.assertEqual( called_kwargs['headers'], - {"X-A2A-Notification-Token": "unique_token"}, + {'X-A2A-Notification-Token': 'unique_token'}, ) self.assertNotIn( 'auth', called_kwargs diff --git a/tests/server/tasks/test_push_notification_sender.py b/tests/server/tasks/test_push_notification_sender.py index 50cfed68..7f547cd0 100644 --- a/tests/server/tasks/test_push_notification_sender.py +++ b/tests/server/tasks/test_push_notification_sender.py @@ -61,14 +61,16 @@ async def test_send_notification_success(self): self.mock_httpx_client.post.assert_awaited_once_with( config.url, json=task_data.model_dump(mode='json', exclude_none=True), - headers=None + headers=None, ) mock_response.raise_for_status.assert_called_once() async def test_send_notification_with_token_success(self): task_id = 'task_send_success' task_data = create_sample_task(task_id=task_id) - config = create_sample_push_config(url='http://notify.me/here', token='unique_token') + config = create_sample_push_config( + url='http://notify.me/here', token='unique_token' + ) self.mock_config_store.get_info.return_value = [config] mock_response = AsyncMock(spec=httpx.Response) @@ -83,7 +85,7 @@ async def test_send_notification_with_token_success(self): self.mock_httpx_client.post.assert_awaited_once_with( config.url, json=task_data.model_dump(mode='json', exclude_none=True), - headers={'X-A2A-Notification-Token': 'unique_token'} + headers={'X-A2A-Notification-Token': 'unique_token'}, ) mock_response.raise_for_status.assert_called_once() @@ -120,7 +122,7 @@ async def test_send_notification_http_status_error( self.mock_httpx_client.post.assert_awaited_once_with( config.url, json=task_data.model_dump(mode='json', exclude_none=True), - headers=None + headers=None, ) mock_logger.error.assert_called_once() @@ -148,12 +150,12 @@ async def test_send_notification_multiple_configs(self): self.mock_httpx_client.post.assert_any_call( config1.url, json=task_data.model_dump(mode='json', exclude_none=True), - headers=None + headers=None, ) # Check calls for config2 self.mock_httpx_client.post.assert_any_call( config2.url, json=task_data.model_dump(mode='json', exclude_none=True), - headers=None + headers=None, ) mock_response.raise_for_status.call_count = 2 diff --git a/tests/server/tasks/test_task_updater.py b/tests/server/tasks/test_task_updater.py index 39b64252..1a633e6a 100644 --- a/tests/server/tasks/test_task_updater.py +++ b/tests/server/tasks/test_task_updater.py @@ -508,7 +508,9 @@ async def test_cancel_with_message(task_updater, event_queue, sample_message): @pytest.mark.asyncio -async def test_update_status_raises_error_if_terminal_state_reached(task_updater, event_queue): +async def test_update_status_raises_error_if_terminal_state_reached( + task_updater, event_queue +): await task_updater.complete() event_queue.reset_mock() with pytest.raises(RuntimeError): @@ -520,8 +522,8 @@ async def test_update_status_raises_error_if_terminal_state_reached(task_updater async def test_concurrent_updates_race_condition(event_queue): task_updater = TaskUpdater( event_queue=event_queue, - task_id="test-task-id", - context_id="test-context-id", + task_id='test-task-id', + context_id='test-context-id', ) tasks = [ task_updater.complete(), diff --git a/tests/test_types.py b/tests/test_types.py index 7b5637a9..1bae000d 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -71,7 +71,7 @@ TaskStatusUpdateEvent, TextPart, UnsupportedOperationError, - GetTaskPushNotificationConfigParams + GetTaskPushNotificationConfigParams, ) @@ -1501,28 +1501,26 @@ def test_subclass_enums() -> None: def test_get_task_push_config_params() -> None: """Tests successful validation of GetTaskPushNotificationConfigParams.""" # Minimal valid data - params = { - "id":"task-1234" - } + params = {'id': 'task-1234'} TaskIdParams.model_validate(params) GetTaskPushNotificationConfigParams.model_validate(params) + def test_use_get_task_push_notification_params_for_request() -> None: # GetTaskPushNotificationConfigRequest get_push_notif_req_data: dict[str, Any] = { 'id': 1, 'jsonrpc': '2.0', 'method': 'tasks/pushNotificationConfig/get', - 'params': { - "id":"task-1234", - "pushNotificationConfigId":"c1" - } + 'params': {'id': 'task-1234', 'pushNotificationConfigId': 'c1'}, } a2a_req_get_push_req = A2ARequest.model_validate(get_push_notif_req_data) assert isinstance( a2a_req_get_push_req.root, GetTaskPushNotificationConfigRequest ) - assert isinstance(a2a_req_get_push_req.root.params, GetTaskPushNotificationConfigParams) + assert isinstance( + a2a_req_get_push_req.root.params, GetTaskPushNotificationConfigParams + ) assert ( a2a_req_get_push_req.root.method == 'tasks/pushNotificationConfig/get' - ) \ No newline at end of file + )