Skip to content

Commit ab762f0

Browse files
authored
fix: Remove unconditional SQLAlchemy dependency from SDK core (#898)
## Description This PR addresses issue #883 where importing components under `a2a.server.request_handlers` (such as `RequestHandler`) inadvertently introduces a hard dependency on `sqlalchemy`. Previously, `a2a.server.request_handlers.response_helpers` imported `a2a.compat.v0_3.conversions`, which brought in `TaskModel` and `PushNotificationConfigModel` from `a2a.server.models`, triggering a `ModuleNotFoundError` for users who did not have the `[sql]` extras installed. This change decouples the database model conversion operations from the standard core-to-compat message conversions by introducing a new `model_conversions.py` module. ## Changes Made - **Created** `src/a2a/compat/v0_3/model_conversions.py` to host SQLAlchemy-specific conversion functions (`core_to_compat_task_model`, `compat_task_model_to_core`, etc.). - **Refactored** `src/a2a/compat/v0_3/conversions.py` to remove unconditional imports of `PushNotificationConfigModel` and `TaskModel`, keeping the module lightweight. - **Updated** internal imports in `database_task_store.py` and `database_push_notification_config_store.py` to route through the new `model_conversions.py` module. - **Updated** related test suites (`test_conversions.py`, `test_database_task_store.py`, `test_database_push_notification_config_store.py`) to align with the new import paths. ## Verification - Confirmed that importing `RequestHandler` directly in a clean virtual environment (without `sqlalchemy` installed) succeeds. `python -m venv venv-test-sql-fix && source venv-test-sql-fix/bin/activate && pip install -e '.' && python -c 'from a2a.server.request_handlers.request_handler import RequestHandler; print(\"Import Successful! No sqlalchemy dependency.\")' && python -c 'try: import sqlalchemy; print(\"sqlalchemy is installed! FAIL\")\nexcept ImportError: print(\"sqlalchemy is NOT installed. SUCCESS\")'` - Successfully passed all automated type checking (`mypy`, `pyright`), formatting (`ruff`), and unit tests (`pytest`). ## Related Issues Fixes #883
1 parent 05cd5e9 commit ab762f0

8 files changed

Lines changed: 101 additions & 86 deletions

File tree

docs/migrations/v1_0/database/zero_downtime.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Enable the v0.3 conversion utilities in your application entry point (e.g., `mai
6262

6363
```python
6464
from a2a.server.tasks import DatabaseTaskStore, DatabasePushNotificationConfigStore
65-
from a2a.compat.v0_3.conversions import (
65+
from a2a.compat.v0_3.model_conversions import (
6666
core_to_compat_task_model,
6767
core_to_compat_push_notification_config_model,
6868
)
@@ -126,7 +126,7 @@ This allows v1.0 instances to read *all* existing data regardless of when it was
126126

127127
## 🧩 Resources
128128
- **[a2a-db CLI](../../../../src/a2a/migrations/README.md)**: The primary tool for executing schema migrations.
129-
- **[Compatibility Conversions](../../../../src/a2a/compat/v0_3/conversions.py)**: Source for classes like `core_to_compat_task_model` used in Step 2.
129+
- **[Compatibility Conversions](../../../../src/a2a/compat/v0_3/model_conversions.py)**: Source for model conversion functions `core_to_compat_task_model` and `core_to_compat_push_notification_config_model` used in Step 2.
130130
- **[Task Store Implementation](../../../../src/a2a/server/tasks/database_task_store.py)**: The `DatabaseTaskStore` which handles the version-aware read/write logic.
131131
- **[Push Notification Store Implementation](../../../../src/a2a/server/tasks/database_push_notification_config_store.py)**: The `DatabasePushNotificationConfigStore` which handles the version-aware read/write logic.
132132

src/a2a/compat/v0_3/conversions.py

Lines changed: 1 addition & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import base64
22

3-
from typing import TYPE_CHECKING, Any
4-
5-
6-
if TYPE_CHECKING:
7-
from cryptography.fernet import Fernet
3+
from typing import Any
84

95
from google.protobuf.json_format import MessageToDict, ParseDict
106

117
from a2a.compat.v0_3 import types as types_v03
128
from a2a.compat.v0_3.versions import is_legacy_version
13-
from a2a.server.models import PushNotificationConfigModel, TaskModel
149
from a2a.types import a2a_pb2 as pb2_v10
1510
from a2a.utils import constants, errors
1611

@@ -1378,77 +1373,3 @@ def to_compat_get_extended_agent_card_request(
13781373
) -> types_v03.GetAuthenticatedExtendedCardRequest:
13791374
"""Convert get extended agent card request to v0.3 compat type."""
13801375
return types_v03.GetAuthenticatedExtendedCardRequest(id=request_id)
1381-
1382-
1383-
def core_to_compat_task_model(task: pb2_v10.Task, owner: str) -> TaskModel:
1384-
"""Converts a 1.0 core Task to a TaskModel using v0.3 JSON structure."""
1385-
compat_task = to_compat_task(task)
1386-
data = compat_task.model_dump(mode='json')
1387-
1388-
return TaskModel(
1389-
id=task.id,
1390-
context_id=task.context_id,
1391-
owner=owner,
1392-
status=data.get('status'),
1393-
history=data.get('history'),
1394-
artifacts=data.get('artifacts'),
1395-
task_metadata=data.get('metadata'),
1396-
protocol_version='0.3',
1397-
)
1398-
1399-
1400-
def compat_task_model_to_core(task_model: TaskModel) -> pb2_v10.Task:
1401-
"""Converts a TaskModel with v0.3 structure to a 1.0 core Task."""
1402-
compat_task = types_v03.Task(
1403-
id=task_model.id,
1404-
context_id=task_model.context_id,
1405-
status=types_v03.TaskStatus.model_validate(task_model.status),
1406-
artifacts=(
1407-
[types_v03.Artifact.model_validate(a) for a in task_model.artifacts]
1408-
if task_model.artifacts
1409-
else []
1410-
),
1411-
history=(
1412-
[types_v03.Message.model_validate(h) for h in task_model.history]
1413-
if task_model.history
1414-
else []
1415-
),
1416-
metadata=task_model.task_metadata,
1417-
)
1418-
return to_core_task(compat_task)
1419-
1420-
1421-
def core_to_compat_push_notification_config_model(
1422-
task_id: str,
1423-
config: pb2_v10.TaskPushNotificationConfig,
1424-
owner: str,
1425-
fernet: 'Fernet | None' = None,
1426-
) -> PushNotificationConfigModel:
1427-
"""Converts a 1.0 core TaskPushNotificationConfig to a PushNotificationConfigModel using v0.3 JSON structure."""
1428-
compat_config = to_compat_push_notification_config(config)
1429-
1430-
json_payload = compat_config.model_dump_json().encode('utf-8')
1431-
data_to_store = fernet.encrypt(json_payload) if fernet else json_payload
1432-
1433-
return PushNotificationConfigModel(
1434-
task_id=task_id,
1435-
config_id=config.id,
1436-
owner=owner,
1437-
config_data=data_to_store,
1438-
protocol_version='0.3',
1439-
)
1440-
1441-
1442-
def compat_push_notification_config_model_to_core(
1443-
model_instance: str, task_id: str
1444-
) -> pb2_v10.TaskPushNotificationConfig:
1445-
"""Converts a PushNotificationConfigModel with v0.3 structure back to a 1.0 core TaskPushNotificationConfig."""
1446-
inner_config = types_v03.PushNotificationConfig.model_validate_json(
1447-
model_instance
1448-
)
1449-
return to_core_task_push_notification_config(
1450-
types_v03.TaskPushNotificationConfig(
1451-
task_id=task_id,
1452-
push_notification_config=inner_config,
1453-
)
1454-
)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Database model conversions for v0.3 compatibility."""
2+
3+
from typing import TYPE_CHECKING
4+
5+
6+
if TYPE_CHECKING:
7+
from cryptography.fernet import Fernet
8+
9+
10+
from a2a.compat.v0_3 import types as types_v03
11+
from a2a.compat.v0_3.conversions import (
12+
to_compat_push_notification_config,
13+
to_compat_task,
14+
to_core_task,
15+
to_core_task_push_notification_config,
16+
)
17+
from a2a.server.models import PushNotificationConfigModel, TaskModel
18+
from a2a.types import a2a_pb2 as pb2_v10
19+
20+
21+
def core_to_compat_task_model(task: pb2_v10.Task, owner: str) -> TaskModel:
22+
"""Converts a 1.0 core Task to a TaskModel using v0.3 JSON structure."""
23+
compat_task = to_compat_task(task)
24+
data = compat_task.model_dump(mode='json')
25+
26+
return TaskModel(
27+
id=task.id,
28+
context_id=task.context_id,
29+
owner=owner,
30+
status=data.get('status'),
31+
history=data.get('history'),
32+
artifacts=data.get('artifacts'),
33+
task_metadata=data.get('metadata'),
34+
protocol_version='0.3',
35+
)
36+
37+
38+
def compat_task_model_to_core(task_model: TaskModel) -> pb2_v10.Task:
39+
"""Converts a TaskModel with v0.3 structure to a 1.0 core Task."""
40+
compat_task = types_v03.Task(
41+
id=task_model.id,
42+
context_id=task_model.context_id,
43+
status=types_v03.TaskStatus.model_validate(task_model.status),
44+
artifacts=(
45+
[types_v03.Artifact.model_validate(a) for a in task_model.artifacts]
46+
if task_model.artifacts
47+
else []
48+
),
49+
history=(
50+
[types_v03.Message.model_validate(h) for h in task_model.history]
51+
if task_model.history
52+
else []
53+
),
54+
metadata=task_model.task_metadata,
55+
)
56+
return to_core_task(compat_task)
57+
58+
59+
def core_to_compat_push_notification_config_model(
60+
task_id: str,
61+
config: pb2_v10.TaskPushNotificationConfig,
62+
owner: str,
63+
fernet: 'Fernet | None' = None,
64+
) -> PushNotificationConfigModel:
65+
"""Converts a 1.0 core TaskPushNotificationConfig to a PushNotificationConfigModel using v0.3 JSON structure."""
66+
compat_config = to_compat_push_notification_config(config)
67+
68+
json_payload = compat_config.model_dump_json().encode('utf-8')
69+
data_to_store = fernet.encrypt(json_payload) if fernet else json_payload
70+
71+
return PushNotificationConfigModel(
72+
task_id=task_id,
73+
config_id=config.id,
74+
owner=owner,
75+
config_data=data_to_store,
76+
protocol_version='0.3',
77+
)
78+
79+
80+
def compat_push_notification_config_model_to_core(
81+
model_instance: str, task_id: str
82+
) -> pb2_v10.TaskPushNotificationConfig:
83+
"""Converts a PushNotificationConfigModel with v0.3 structure back to a 1.0 core TaskPushNotificationConfig."""
84+
inner_config = types_v03.PushNotificationConfig.model_validate_json(
85+
model_instance
86+
)
87+
return to_core_task_push_notification_config(
88+
types_v03.TaskPushNotificationConfig(
89+
task_id=task_id,
90+
push_notification_config=inner_config,
91+
)
92+
)

src/a2a/server/tasks/database_push_notification_config_store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
from collections.abc import Callable
2828

29-
from a2a.compat.v0_3.conversions import (
29+
from a2a.compat.v0_3.model_conversions import (
3030
compat_push_notification_config_model_to_core,
3131
)
3232
from a2a.server.context import ServerCallContext

src/a2a/server/tasks/database_task_store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
) from e
2424
from google.protobuf.json_format import MessageToDict, ParseDict
2525

26-
from a2a.compat.v0_3.conversions import (
26+
from a2a.compat.v0_3.model_conversions import (
2727
compat_task_model_to_core,
2828
)
2929
from a2a.server.context import ServerCallContext

tests/compat/v0_3/test_conversions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
to_core_task_push_notification_config,
7474
to_core_task_status,
7575
to_core_task_status_update_event,
76+
)
77+
from a2a.compat.v0_3.model_conversions import (
7678
core_to_compat_task_model,
7779
compat_task_model_to_core,
7880
core_to_compat_push_notification_config_model,

tests/server/tasks/test_database_push_notification_config_store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
TaskState,
4545
TaskStatus,
4646
)
47-
from a2a.compat.v0_3.conversions import (
47+
from a2a.compat.v0_3.model_conversions import (
4848
core_to_compat_push_notification_config_model,
4949
)
5050

tests/server/tasks/test_database_task_store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
from a2a.server.models import Base, TaskModel # Important: To get Base.metadata
2626
from a2a.server.tasks.database_task_store import DatabaseTaskStore
27-
from a2a.compat.v0_3.conversions import core_to_compat_task_model
27+
from a2a.compat.v0_3.model_conversions import core_to_compat_task_model
2828
from a2a.types.a2a_pb2 import (
2929
Artifact,
3030
ListTasksRequest,

0 commit comments

Comments
 (0)