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.
- {smarta2a-0.2.0 → smarta2a-0.2.1}/PKG-INFO +1 -1
- {smarta2a-0.2.0 → smarta2a-0.2.1}/pyproject.toml +1 -1
- {smarta2a-0.2.0 → smarta2a-0.2.1}/smarta2a/server.py +117 -0
- {smarta2a-0.2.0 → smarta2a-0.2.1}/tests/test_server.py +149 -1
- {smarta2a-0.2.0 → smarta2a-0.2.1}/.gitignore +0 -0
- {smarta2a-0.2.0 → smarta2a-0.2.1}/LICENSE +0 -0
- {smarta2a-0.2.0 → smarta2a-0.2.1}/README.md +0 -0
- {smarta2a-0.2.0 → smarta2a-0.2.1}/requirements.txt +0 -0
- {smarta2a-0.2.0 → smarta2a-0.2.1}/smarta2a/__init__.py +0 -0
- {smarta2a-0.2.0 → smarta2a-0.2.1}/smarta2a/types.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: smarta2a
|
3
|
-
Version: 0.2.
|
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
|
@@ -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
|