smarta2a 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smarta2a
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A Python package for creating servers and clients following Google's Agent2Agent protocol
5
5
  Project-URL: Homepage, https://github.com/siddharthsma/smarta2a
6
6
  Project-URL: Bug Tracker, https://github.com/siddharthsma/smarta2a/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "smarta2a"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  authors = [
9
9
  { name = "Siddharth Ambegaonkar", email = "siddharthsma@gmail.com" },
10
10
  ]
@@ -48,6 +48,11 @@ from .types import (
48
48
  A2AStatus,
49
49
  A2AStreamResponse,
50
50
  TaskSendParams,
51
+ SetTaskPushNotificationRequest,
52
+ GetTaskPushNotificationRequest,
53
+ SetTaskPushNotificationResponse,
54
+ GetTaskPushNotificationResponse,
55
+ TaskPushNotificationConfig,
51
56
  )
52
57
 
53
58
  class SmartA2A:
@@ -129,6 +134,18 @@ class SmartA2A:
129
134
  self._register_handler("tasks/cancel", func, "task_cancel", "handler")
130
135
  return func
131
136
  return decorator
137
+
138
+ def set_notification(self):
139
+ def decorator(func: Callable[[SetTaskPushNotificationRequest], None]) -> Callable:
140
+ self._register_handler("tasks/pushNotification/set", func, "set_notification", "handler")
141
+ return func
142
+ return decorator
143
+
144
+ def get_notification(self):
145
+ def decorator(func: Callable[[GetTaskPushNotificationRequest], Union[TaskPushNotificationConfig, GetTaskPushNotificationResponse]]):
146
+ self._register_handler("tasks/pushNotification/get", func, "get_notification", "handler")
147
+ return func
148
+ return decorator
132
149
 
133
150
  async def process_request(self, request_data: dict) -> JSONRPCResponse:
134
151
  try:
@@ -141,6 +158,10 @@ class SmartA2A:
141
158
  return self._handle_get_task(request_data)
142
159
  elif method == "tasks/cancel":
143
160
  return self._handle_cancel_task(request_data)
161
+ elif method == "tasks/pushNotification/set":
162
+ return self._handle_set_notification(request_data)
163
+ elif method == "tasks/pushNotification/get":
164
+ return self._handle_get_notification(request_data)
144
165
  else:
145
166
  return self._error_response(
146
167
  request_data.get("id"),
@@ -422,6 +443,102 @@ class SmartA2A:
422
443
  error=InternalError(data=str(e))
423
444
  )
424
445
 
446
+ def _handle_set_notification(self, request_data: dict) -> SetTaskPushNotificationResponse:
447
+ try:
448
+ request = SetTaskPushNotificationRequest.model_validate(request_data)
449
+ handler = self.handlers.get("tasks/pushNotification/set")
450
+
451
+ if not handler:
452
+ return SetTaskPushNotificationResponse(
453
+ id=request.id,
454
+ error=MethodNotFoundError()
455
+ )
456
+
457
+ try:
458
+ # Execute handler (may or may not return something)
459
+ raw_result = handler(request)
460
+
461
+ # If handler returns nothing - build success response from request params
462
+ if raw_result is None:
463
+ return SetTaskPushNotificationResponse(
464
+ id=request.id,
465
+ result=request.params
466
+ )
467
+
468
+ # If handler returns a full response object
469
+ if isinstance(raw_result, SetTaskPushNotificationResponse):
470
+ return raw_result
471
+
472
+
473
+ except Exception as e:
474
+ if isinstance(e, JSONRPCError):
475
+ return SetTaskPushNotificationResponse(
476
+ id=request.id,
477
+ error=e
478
+ )
479
+ return SetTaskPushNotificationResponse(
480
+ id=request.id,
481
+ error=InternalError(data=str(e))
482
+ )
483
+
484
+ except ValidationError as e:
485
+ return SetTaskPushNotificationResponse(
486
+ id=request_data.get("id"),
487
+ error=InvalidRequestError(data=e.errors())
488
+ )
489
+
490
+
491
+ def _handle_get_notification(self, request_data: dict) -> GetTaskPushNotificationResponse:
492
+ try:
493
+ request = GetTaskPushNotificationRequest.model_validate(request_data)
494
+ handler = self.handlers.get("tasks/pushNotification/get")
495
+
496
+ if not handler:
497
+ return GetTaskPushNotificationResponse(
498
+ id=request.id,
499
+ error=MethodNotFoundError()
500
+ )
501
+
502
+ try:
503
+ raw_result = handler(request)
504
+
505
+ if isinstance(raw_result, GetTaskPushNotificationResponse):
506
+ return raw_result
507
+ else:
508
+ # Validate raw_result as TaskPushNotificationConfig
509
+ config = TaskPushNotificationConfig.model_validate(raw_result)
510
+ return GetTaskPushNotificationResponse(
511
+ id=request.id,
512
+ result=config
513
+ )
514
+ except ValidationError as e:
515
+ return GetTaskPushNotificationResponse(
516
+ id=request.id,
517
+ error=InvalidParamsError(data=e.errors())
518
+ )
519
+ except Exception as e:
520
+ if isinstance(e, JSONRPCError):
521
+ return GetTaskPushNotificationResponse(
522
+ id=request.id,
523
+ error=e
524
+ )
525
+ return GetTaskPushNotificationResponse(
526
+ id=request.id,
527
+ error=InternalError(data=str(e))
528
+ )
529
+
530
+ except ValidationError as e:
531
+ return GetTaskPushNotificationResponse(
532
+ id=request_data.get("id"),
533
+ error=InvalidRequestError(data=e.errors())
534
+ )
535
+ except json.JSONDecodeError as e:
536
+ return GetTaskPushNotificationResponse(
537
+ id=request_data.get("id"),
538
+ error=JSONParseError(data=str(e))
539
+ )
540
+
541
+
425
542
  def _normalize_artifacts(self, content: Any) -> List[Artifact]:
426
543
  """Handle both A2AResponse content and regular returns"""
427
544
  if isinstance(content, Artifact):
@@ -24,7 +24,14 @@ from smarta2a.types import (
24
24
  A2AStatus,
25
25
  A2AStreamResponse,
26
26
  SendTaskResponse,
27
- Message
27
+ Message,
28
+ InternalError,
29
+ TaskNotFoundError,
30
+ SetTaskPushNotificationRequest,
31
+ GetTaskPushNotificationRequest,
32
+ SetTaskPushNotificationResponse,
33
+ GetTaskPushNotificationResponse,
34
+ TaskPushNotificationConfig
28
35
  )
29
36
 
30
37
  @pytest.fixture
@@ -500,4 +507,145 @@ def test_send_task_content_access():
500
507
  assert request.content == request.params.message.parts
501
508
 
502
509
 
510
+ def test_set_notification_success(a2a_server, client):
511
+ # Test basic success case with no return value
512
+ @a2a_server.set_notification()
513
+ def handle_set(req: SetTaskPushNotificationRequest):
514
+ # No return needed - just validate request
515
+ assert req.params.id == "test123"
516
+
517
+ request_data = {
518
+ "jsonrpc": "2.0",
519
+ "id": 1,
520
+ "method": "tasks/pushNotification/set",
521
+ "params": {
522
+ "id": "test123",
523
+ "pushNotificationConfig": {
524
+ "url": "https://example.com/callback",
525
+ "authentication": {
526
+ "schemes": ["jwt"]
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+ response = client.post("/", json=request_data).json()
533
+
534
+ assert response["result"]["id"] == "test123"
535
+ assert response["result"]["pushNotificationConfig"]["url"] == request_data["params"]["pushNotificationConfig"]["url"]
536
+ assert response["result"]["pushNotificationConfig"]["authentication"]["schemes"] == ["jwt"]
537
+
538
+ def test_set_notification_custom_response(a2a_server, client):
539
+ # Test handler returning custom response
540
+ @a2a_server.set_notification()
541
+ def handle_set(req):
542
+ return SetTaskPushNotificationResponse(
543
+ id=req.id,
544
+ result=TaskPushNotificationConfig(
545
+ id="test123",
546
+ pushNotificationConfig={
547
+ "url": "custom-url",
548
+ "token": "secret"
549
+ }
550
+ )
551
+ )
552
+
553
+ response = client.post("/", json={
554
+ "jsonrpc": "2.0",
555
+ "id": 2,
556
+ "method": "tasks/pushNotification/set",
557
+ "params": {
558
+ "id": "test123",
559
+ "pushNotificationConfig": {"url": "https://example.com"}
560
+ }
561
+ }).json()
562
+
563
+ assert response["result"]["pushNotificationConfig"]["url"] == "custom-url"
564
+ assert "secret" in response["result"]["pushNotificationConfig"]["token"]
565
+
566
+
567
+ # --- Get Notification Tests ---
568
+
569
+ def test_get_notification_success(a2a_server, client):
570
+ # Test successful config retrieval
571
+ @a2a_server.get_notification()
572
+ def handle_get(req: GetTaskPushNotificationRequest):
573
+ return TaskPushNotificationConfig(
574
+ id=req.params.id,
575
+ pushNotificationConfig={
576
+ "url": "https://test.com",
577
+ "token": "abc123"
578
+ }
579
+ )
580
+
581
+ request_data = {
582
+ "jsonrpc": "2.0",
583
+ "id": 4,
584
+ "method": "tasks/pushNotification/get",
585
+ "params": {"id": "test456"}
586
+ }
587
+
588
+ response = client.post("/", json=request_data).json()
589
+
590
+ assert response["result"]["id"] == "test456"
591
+ assert response["result"]["pushNotificationConfig"]["url"] == "https://test.com"
592
+
593
+ def test_get_notification_direct_response(a2a_server, client):
594
+ # Test handler returning full response object
595
+ @a2a_server.get_notification()
596
+ def handle_get(req):
597
+ return GetTaskPushNotificationResponse(
598
+ id=req.id,
599
+ result=TaskPushNotificationConfig(
600
+ id=req.params.id,
601
+ pushNotificationConfig={
602
+ "url": "direct-response.example",
603
+ "authentication": {"schemes": ["basic"]}
604
+ }
605
+ )
606
+ )
607
+
608
+ response = client.post("/", json={
609
+ "jsonrpc": "2.0",
610
+ "id": 5,
611
+ "method": "tasks/pushNotification/get",
612
+ "params": {"id": "test789"}
613
+ }).json()
614
+
615
+ assert "direct-response" in response["result"]["pushNotificationConfig"]["url"]
616
+ assert "basic" in response["result"]["pushNotificationConfig"]["authentication"]["schemes"]
617
+
618
+ def test_get_notification_validation_error(a2a_server, client):
619
+ # Test invalid response from handler
620
+ @a2a_server.get_notification()
621
+ def handle_get(req):
622
+ return {"invalid": "config"}
623
+
624
+ response = client.post("/", json={
625
+ "jsonrpc": "2.0",
626
+ "id": 6,
627
+ "method": "tasks/pushNotification/get",
628
+ "params": {"id": "test999"}
629
+ }).json()
630
+
631
+ assert response["error"]["code"] == -32602 # Invalid params
632
+
633
+
634
+ def test_get_notification_error_propagation(a2a_server, client):
635
+ # Test exception handling
636
+ @a2a_server.get_notification()
637
+ def handle_get(req):
638
+ raise InternalError(message="Storage failure")
639
+
640
+ response = client.post("/", json={
641
+ "jsonrpc": "2.0",
642
+ "id": 7,
643
+ "method": "tasks/pushNotification/get",
644
+ "params": {"id": "test-error"}
645
+ }).json()
646
+
647
+ assert response["error"]["code"] == -32603 # Internal error code
648
+
649
+
650
+
503
651
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes