smarta2a 0.1.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,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smarta2a
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: A Python package for creating servers and clients following Google's Agent2Agent protocol
5
- Project-URL: Homepage, https://github.com/siddharthsma/fasta2a
6
- Project-URL: Bug Tracker, https://github.com/siddharthsma/fasta2a/issues
5
+ Project-URL: Homepage, https://github.com/siddharthsma/smarta2a
6
+ Project-URL: Bug Tracker, https://github.com/siddharthsma/smarta2a/issues
7
7
  Author-email: Siddharth Ambegaonkar <siddharthsma@gmail.com>
8
8
  License-File: LICENSE
9
9
  Classifier: License :: OSI Approved :: MIT License
@@ -16,7 +16,7 @@ Requires-Dist: sse-starlette
16
16
  Requires-Dist: uvicorn
17
17
  Description-Content-Type: text/markdown
18
18
 
19
- # FastA2A
19
+ # SmartA2A
20
20
 
21
21
  A Python package for creating a server following Google's Agent2Agent protocol
22
22
 
@@ -39,15 +39,15 @@ A Python package for creating a server following Google's Agent2Agent protocol
39
39
  ## Installation
40
40
 
41
41
  ```bash
42
- pip install fasta2a
42
+ pip install smarta2a
43
43
  ```
44
44
 
45
45
  ## Simple Echo Server Implementation
46
46
 
47
47
  ```python
48
- from fasta2a import FastA2A
48
+ from smarta2a import SmartA2A
49
49
 
50
- app = FastA2A("EchoServer")
50
+ app = SmartA2A("EchoServer")
51
51
 
52
52
  @app.on_send_task()
53
53
  def handle_task(request):
@@ -81,8 +81,8 @@ To set up the development environment:
81
81
 
82
82
  ```bash
83
83
  # Clone the repository
84
- git clone https://github.com/siddharthsma/fasta2a.git
85
- cd fasta2a
84
+ git clone https://github.com/siddharthsma/smarta2a.git
85
+ cd smarta2a
86
86
 
87
87
  # Create and activate virtual environment
88
88
  python -m venv venv
@@ -1,4 +1,4 @@
1
- # FastA2A
1
+ # SmartA2A
2
2
 
3
3
  A Python package for creating a server following Google's Agent2Agent protocol
4
4
 
@@ -21,15 +21,15 @@ A Python package for creating a server following Google's Agent2Agent protocol
21
21
  ## Installation
22
22
 
23
23
  ```bash
24
- pip install fasta2a
24
+ pip install smarta2a
25
25
  ```
26
26
 
27
27
  ## Simple Echo Server Implementation
28
28
 
29
29
  ```python
30
- from fasta2a import FastA2A
30
+ from smarta2a import SmartA2A
31
31
 
32
- app = FastA2A("EchoServer")
32
+ app = SmartA2A("EchoServer")
33
33
 
34
34
  @app.on_send_task()
35
35
  def handle_task(request):
@@ -63,8 +63,8 @@ To set up the development environment:
63
63
 
64
64
  ```bash
65
65
  # Clone the repository
66
- git clone https://github.com/siddharthsma/fasta2a.git
67
- cd fasta2a
66
+ git clone https://github.com/siddharthsma/smarta2a.git
67
+ cd smarta2a
68
68
 
69
69
  # Create and activate virtual environment
70
70
  python -m venv venv
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "smarta2a"
7
- version = "0.1.0"
7
+ version = "0.2.1"
8
8
  authors = [
9
9
  { name = "Siddharth Ambegaonkar", email = "siddharthsma@gmail.com" },
10
10
  ]
@@ -24,11 +24,11 @@ dependencies = [
24
24
  ]
25
25
 
26
26
  [project.urls]
27
- "Homepage" = "https://github.com/siddharthsma/fasta2a"
28
- "Bug Tracker" = "https://github.com/siddharthsma/fasta2a/issues"
27
+ "Homepage" = "https://github.com/siddharthsma/smarta2a"
28
+ "Bug Tracker" = "https://github.com/siddharthsma/smarta2a/issues"
29
29
 
30
30
  [tool.hatch.build.targets.wheel]
31
- packages = ["fasta2a"]
31
+ packages = ["smarta2a"]
32
32
 
33
33
  [tool.pytest.ini_options]
34
34
  testpaths = ["tests"]
@@ -3,7 +3,7 @@ anyio==4.9.0
3
3
  certifi==2025.1.31
4
4
  charset-normalizer==3.4.1
5
5
  click==8.1.8
6
- -e git+https://github.com/siddharthsma/fasta2a.git@74f00c9dc2d300bf5808ea82d8caba7b1f16a932#egg=fasta2a
6
+ -e git+https://github.com/siddharthsma/smarta2a.git@main#egg=smarta2a
7
7
  fastapi==0.115.12
8
8
  h11==0.14.0
9
9
  httpcore==1.0.8
@@ -4,6 +4,7 @@ py_a2a - A Python package for implementing an A2A server
4
4
 
5
5
  __version__ = "0.1.0"
6
6
 
7
- from .server import FastA2A
7
+ from .server import SmartA2A
8
+ from . import types as models
8
9
 
9
- __all__ = ["FastA2A", "models"]
10
+ __all__ = ["SmartA2A", "models"]
@@ -48,9 +48,14 @@ 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
- class FastA2A:
58
+ class SmartA2A:
54
59
  def __init__(self, name: str, **fastapi_kwargs):
55
60
  self.name = name
56
61
  self.handlers: Dict[str, Callable] = {}
@@ -96,7 +101,7 @@ class FastA2A:
96
101
  if method in self._registered_decorators:
97
102
  raise RuntimeError(
98
103
  f"@{handler_name} decorator for method '{method}' "
99
- f"can only be used once per FastA2A instance"
104
+ f"can only be used once per SmartA2A instance"
100
105
  )
101
106
 
102
107
  if handler_type == "handler":
@@ -129,6 +134,18 @@ class FastA2A:
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 FastA2A:
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 FastA2A:
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):
@@ -2,8 +2,8 @@ import pytest
2
2
  import json
3
3
  import requests
4
4
  from fastapi.testclient import TestClient
5
- from fasta2a import FastA2A
6
- from fasta2a.types import (
5
+ from smarta2a import SmartA2A
6
+ from smarta2a.types import (
7
7
  TaskSendParams,
8
8
  SendTaskRequest,
9
9
  GetTaskRequest,
@@ -24,12 +24,19 @@ from fasta2a.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
31
38
  def a2a_server():
32
- server = FastA2A("test-server")
39
+ server = SmartA2A("test-server")
33
40
  return server
34
41
 
35
42
  @pytest.fixture
@@ -448,7 +455,7 @@ def test_send_subscribe_task(client, a2a_server):
448
455
 
449
456
  def test_duplicate_on_send_task_registration():
450
457
  """Test that @on_send_task can only be registered once"""
451
- app = FastA2A("test-app")
458
+ app = SmartA2A("test-app")
452
459
 
453
460
  # First registration should work
454
461
  @app.on_send_task()
@@ -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
 
smarta2a-0.1.0/setup.py DELETED
@@ -1,25 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
- with open("README.md", "r", encoding="utf-8") as fh:
4
- long_description = fh.read()
5
-
6
- setup(
7
- name="fasta2a",
8
- version="0.1.0",
9
- author="Siddharth Ambegaonkar",
10
- author_email="sid.ambegaonkar@gmail.com",
11
- description="A Python package for implementing an A2A server",
12
- long_description=long_description,
13
- long_description_content_type="text/markdown",
14
- url="https://github.com/sambegaonkar/py-a2a",
15
- packages=find_packages(),
16
- classifiers=[
17
- "Programming Language :: Python :: 3",
18
- "License :: OSI Approved :: MIT License",
19
- "Operating System :: OS Independent",
20
- ],
21
- python_requires=">=3.7",
22
- install_requires=[
23
- # Add your dependencies here
24
- ],
25
- )
File without changes
File without changes