diff --git a/docs/migrations/v1_0/database/zero_downtime.md b/docs/migrations/v1_0/database/zero_downtime.md index 3278c3265..026ec88c1 100644 --- a/docs/migrations/v1_0/database/zero_downtime.md +++ b/docs/migrations/v1_0/database/zero_downtime.md @@ -62,7 +62,7 @@ Enable the v0.3 conversion utilities in your application entry point (e.g., `mai ```python from a2a.server.tasks import DatabaseTaskStore, DatabasePushNotificationConfigStore -from a2a.compat.v0_3.conversions import ( +from a2a.compat.v0_3.model_conversions import ( core_to_compat_task_model, core_to_compat_push_notification_config_model, ) @@ -126,7 +126,7 @@ This allows v1.0 instances to read *all* existing data regardless of when it was ## 🧩 Resources - **[a2a-db CLI](../../../../src/a2a/migrations/README.md)**: The primary tool for executing schema migrations. -- **[Compatibility Conversions](../../../../src/a2a/compat/v0_3/conversions.py)**: Source for classes like `core_to_compat_task_model` used in Step 2. +- **[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. - **[Task Store Implementation](../../../../src/a2a/server/tasks/database_task_store.py)**: The `DatabaseTaskStore` which handles the version-aware read/write logic. - **[Push Notification Store Implementation](../../../../src/a2a/server/tasks/database_push_notification_config_store.py)**: The `DatabasePushNotificationConfigStore` which handles the version-aware read/write logic. diff --git a/src/a2a/compat/v0_3/conversions.py b/src/a2a/compat/v0_3/conversions.py index 3f5420198..5945380e9 100644 --- a/src/a2a/compat/v0_3/conversions.py +++ b/src/a2a/compat/v0_3/conversions.py @@ -1,16 +1,11 @@ import base64 -from typing import TYPE_CHECKING, Any - - -if TYPE_CHECKING: - from cryptography.fernet import Fernet +from typing import Any from google.protobuf.json_format import MessageToDict, ParseDict from a2a.compat.v0_3 import types as types_v03 from a2a.compat.v0_3.versions import is_legacy_version -from a2a.server.models import PushNotificationConfigModel, TaskModel from a2a.types import a2a_pb2 as pb2_v10 from a2a.utils import constants, errors @@ -1378,77 +1373,3 @@ def to_compat_get_extended_agent_card_request( ) -> types_v03.GetAuthenticatedExtendedCardRequest: """Convert get extended agent card request to v0.3 compat type.""" return types_v03.GetAuthenticatedExtendedCardRequest(id=request_id) - - -def core_to_compat_task_model(task: pb2_v10.Task, owner: str) -> TaskModel: - """Converts a 1.0 core Task to a TaskModel using v0.3 JSON structure.""" - compat_task = to_compat_task(task) - data = compat_task.model_dump(mode='json') - - return TaskModel( - id=task.id, - context_id=task.context_id, - owner=owner, - status=data.get('status'), - history=data.get('history'), - artifacts=data.get('artifacts'), - task_metadata=data.get('metadata'), - protocol_version='0.3', - ) - - -def compat_task_model_to_core(task_model: TaskModel) -> pb2_v10.Task: - """Converts a TaskModel with v0.3 structure to a 1.0 core Task.""" - compat_task = types_v03.Task( - id=task_model.id, - context_id=task_model.context_id, - status=types_v03.TaskStatus.model_validate(task_model.status), - artifacts=( - [types_v03.Artifact.model_validate(a) for a in task_model.artifacts] - if task_model.artifacts - else [] - ), - history=( - [types_v03.Message.model_validate(h) for h in task_model.history] - if task_model.history - else [] - ), - metadata=task_model.task_metadata, - ) - return to_core_task(compat_task) - - -def core_to_compat_push_notification_config_model( - task_id: str, - config: pb2_v10.TaskPushNotificationConfig, - owner: str, - fernet: 'Fernet | None' = None, -) -> PushNotificationConfigModel: - """Converts a 1.0 core TaskPushNotificationConfig to a PushNotificationConfigModel using v0.3 JSON structure.""" - compat_config = to_compat_push_notification_config(config) - - json_payload = compat_config.model_dump_json().encode('utf-8') - data_to_store = fernet.encrypt(json_payload) if fernet else json_payload - - return PushNotificationConfigModel( - task_id=task_id, - config_id=config.id, - owner=owner, - config_data=data_to_store, - protocol_version='0.3', - ) - - -def compat_push_notification_config_model_to_core( - model_instance: str, task_id: str -) -> pb2_v10.TaskPushNotificationConfig: - """Converts a PushNotificationConfigModel with v0.3 structure back to a 1.0 core TaskPushNotificationConfig.""" - inner_config = types_v03.PushNotificationConfig.model_validate_json( - model_instance - ) - return to_core_task_push_notification_config( - types_v03.TaskPushNotificationConfig( - task_id=task_id, - push_notification_config=inner_config, - ) - ) diff --git a/src/a2a/compat/v0_3/model_conversions.py b/src/a2a/compat/v0_3/model_conversions.py new file mode 100644 index 000000000..9b3cc44f8 --- /dev/null +++ b/src/a2a/compat/v0_3/model_conversions.py @@ -0,0 +1,92 @@ +"""Database model conversions for v0.3 compatibility.""" + +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from cryptography.fernet import Fernet + + +from a2a.compat.v0_3 import types as types_v03 +from a2a.compat.v0_3.conversions import ( + to_compat_push_notification_config, + to_compat_task, + to_core_task, + to_core_task_push_notification_config, +) +from a2a.server.models import PushNotificationConfigModel, TaskModel +from a2a.types import a2a_pb2 as pb2_v10 + + +def core_to_compat_task_model(task: pb2_v10.Task, owner: str) -> TaskModel: + """Converts a 1.0 core Task to a TaskModel using v0.3 JSON structure.""" + compat_task = to_compat_task(task) + data = compat_task.model_dump(mode='json') + + return TaskModel( + id=task.id, + context_id=task.context_id, + owner=owner, + status=data.get('status'), + history=data.get('history'), + artifacts=data.get('artifacts'), + task_metadata=data.get('metadata'), + protocol_version='0.3', + ) + + +def compat_task_model_to_core(task_model: TaskModel) -> pb2_v10.Task: + """Converts a TaskModel with v0.3 structure to a 1.0 core Task.""" + compat_task = types_v03.Task( + id=task_model.id, + context_id=task_model.context_id, + status=types_v03.TaskStatus.model_validate(task_model.status), + artifacts=( + [types_v03.Artifact.model_validate(a) for a in task_model.artifacts] + if task_model.artifacts + else [] + ), + history=( + [types_v03.Message.model_validate(h) for h in task_model.history] + if task_model.history + else [] + ), + metadata=task_model.task_metadata, + ) + return to_core_task(compat_task) + + +def core_to_compat_push_notification_config_model( + task_id: str, + config: pb2_v10.TaskPushNotificationConfig, + owner: str, + fernet: 'Fernet | None' = None, +) -> PushNotificationConfigModel: + """Converts a 1.0 core TaskPushNotificationConfig to a PushNotificationConfigModel using v0.3 JSON structure.""" + compat_config = to_compat_push_notification_config(config) + + json_payload = compat_config.model_dump_json().encode('utf-8') + data_to_store = fernet.encrypt(json_payload) if fernet else json_payload + + return PushNotificationConfigModel( + task_id=task_id, + config_id=config.id, + owner=owner, + config_data=data_to_store, + protocol_version='0.3', + ) + + +def compat_push_notification_config_model_to_core( + model_instance: str, task_id: str +) -> pb2_v10.TaskPushNotificationConfig: + """Converts a PushNotificationConfigModel with v0.3 structure back to a 1.0 core TaskPushNotificationConfig.""" + inner_config = types_v03.PushNotificationConfig.model_validate_json( + model_instance + ) + return to_core_task_push_notification_config( + types_v03.TaskPushNotificationConfig( + task_id=task_id, + push_notification_config=inner_config, + ) + ) diff --git a/src/a2a/server/tasks/database_push_notification_config_store.py b/src/a2a/server/tasks/database_push_notification_config_store.py index 406805445..31cd676c8 100644 --- a/src/a2a/server/tasks/database_push_notification_config_store.py +++ b/src/a2a/server/tasks/database_push_notification_config_store.py @@ -26,7 +26,7 @@ from collections.abc import Callable -from a2a.compat.v0_3.conversions import ( +from a2a.compat.v0_3.model_conversions import ( compat_push_notification_config_model_to_core, ) from a2a.server.context import ServerCallContext diff --git a/src/a2a/server/tasks/database_task_store.py b/src/a2a/server/tasks/database_task_store.py index ac1cf947b..2c95da2ca 100644 --- a/src/a2a/server/tasks/database_task_store.py +++ b/src/a2a/server/tasks/database_task_store.py @@ -23,7 +23,7 @@ ) from e from google.protobuf.json_format import MessageToDict, ParseDict -from a2a.compat.v0_3.conversions import ( +from a2a.compat.v0_3.model_conversions import ( compat_task_model_to_core, ) from a2a.server.context import ServerCallContext diff --git a/tests/compat/v0_3/test_conversions.py b/tests/compat/v0_3/test_conversions.py index 3b66f748c..78a6d563b 100644 --- a/tests/compat/v0_3/test_conversions.py +++ b/tests/compat/v0_3/test_conversions.py @@ -73,6 +73,8 @@ to_core_task_push_notification_config, to_core_task_status, to_core_task_status_update_event, +) +from a2a.compat.v0_3.model_conversions import ( core_to_compat_task_model, compat_task_model_to_core, core_to_compat_push_notification_config_model, diff --git a/tests/server/tasks/test_database_push_notification_config_store.py b/tests/server/tasks/test_database_push_notification_config_store.py index f9f8ad7b1..b13a5cf55 100644 --- a/tests/server/tasks/test_database_push_notification_config_store.py +++ b/tests/server/tasks/test_database_push_notification_config_store.py @@ -44,7 +44,7 @@ TaskState, TaskStatus, ) -from a2a.compat.v0_3.conversions import ( +from a2a.compat.v0_3.model_conversions import ( core_to_compat_push_notification_config_model, ) diff --git a/tests/server/tasks/test_database_task_store.py b/tests/server/tasks/test_database_task_store.py index 445a45a37..ff2ab1938 100644 --- a/tests/server/tasks/test_database_task_store.py +++ b/tests/server/tasks/test_database_task_store.py @@ -24,7 +24,7 @@ from a2a.server.models import Base, TaskModel # Important: To get Base.metadata from a2a.server.tasks.database_task_store import DatabaseTaskStore -from a2a.compat.v0_3.conversions import core_to_compat_task_model +from a2a.compat.v0_3.model_conversions import core_to_compat_task_model from a2a.types.a2a_pb2 import ( Artifact, ListTasksRequest,