diff --git a/ebay_client/core/http/transport.py b/ebay_client/core/http/transport.py index f2ef26e..34d0098 100644 --- a/ebay_client/core/http/transport.py +++ b/ebay_client/core/http/transport.py @@ -69,6 +69,8 @@ class ApiTransport: response = self.request(method, path, **kwargs) if response.status_code == 204: return None + if not response.content or not response.content.strip(): + return None try: return response.json() except ValueError as exc: diff --git a/ebay_client/notification/client.py b/ebay_client/notification/client.py index 5d7c362..4f73768 100644 --- a/ebay_client/notification/client.py +++ b/ebay_client/notification/client.py @@ -2,10 +2,16 @@ from __future__ import annotations from ebay_client.core.http.transport import ApiTransport from ebay_client.generated.notification.models import ( + Config, + CreateSubscriptionFilterRequest, CreateSubscriptionRequest, + Destination, DestinationRequest, DestinationSearchResponse, PublicKey, + Subscription, + SubscriptionFilter, + UpdateSubscriptionRequest, SubscriptionSearchResponse, Topic, TopicSearchResponse, @@ -20,6 +26,23 @@ class NotificationClient: def __init__(self, transport: ApiTransport) -> None: self.transport = transport + def get_config(self) -> Config: + return self.transport.request_model( + Config, + "GET", + "/commerce/notification/v1/config", + scopes=[NOTIFICATION_SCOPE], + ) + + def update_config(self, payload: Config) -> None: + self.transport.request_json( + "PUT", + "/commerce/notification/v1/config", + scopes=[NOTIFICATION_SCOPE], + headers={"Content-Type": "application/json"}, + json_body=payload.model_dump(by_alias=True, exclude_none=True), + ) + def get_topics(self, *, limit: int | None = None, continuation_token: str | None = None) -> TopicSearchResponse: return self.transport.request_model( TopicSearchResponse, @@ -55,6 +78,30 @@ class NotificationClient: json_body=payload.model_dump(by_alias=True, exclude_none=True), ) + def get_destination(self, destination_id: str) -> Destination: + return self.transport.request_model( + Destination, + "GET", + f"/commerce/notification/v1/destination/{destination_id}", + scopes=[NOTIFICATION_SCOPE], + ) + + def update_destination(self, destination_id: str, payload: DestinationRequest) -> None: + self.transport.request_json( + "PUT", + f"/commerce/notification/v1/destination/{destination_id}", + scopes=[NOTIFICATION_SCOPE], + headers={"Content-Type": "application/json"}, + json_body=payload.model_dump(by_alias=True, exclude_none=True), + ) + + def delete_destination(self, destination_id: str) -> None: + self.transport.request_json( + "DELETE", + f"/commerce/notification/v1/destination/{destination_id}", + scopes=[NOTIFICATION_SCOPE], + ) + def get_subscriptions(self, *, limit: int | None = None, continuation_token: str | None = None) -> SubscriptionSearchResponse: return self.transport.request_model( SubscriptionSearchResponse, @@ -73,6 +120,72 @@ class NotificationClient: json_body=payload.model_dump(by_alias=True, exclude_none=True), ) + def get_subscription(self, subscription_id: str) -> Subscription: + return self.transport.request_model( + Subscription, + "GET", + f"/commerce/notification/v1/subscription/{subscription_id}", + scopes=[NOTIFICATION_SCOPE, NOTIFICATION_SUBSCRIPTION_READ_SCOPE], + ) + + def update_subscription(self, subscription_id: str, payload: UpdateSubscriptionRequest) -> None: + self.transport.request_json( + "PUT", + f"/commerce/notification/v1/subscription/{subscription_id}", + scopes=[NOTIFICATION_SCOPE, NOTIFICATION_SUBSCRIPTION_SCOPE], + headers={"Content-Type": "application/json"}, + json_body=payload.model_dump(by_alias=True, exclude_none=True), + ) + + def delete_subscription(self, subscription_id: str) -> None: + self.transport.request_json( + "DELETE", + f"/commerce/notification/v1/subscription/{subscription_id}", + scopes=[NOTIFICATION_SCOPE, NOTIFICATION_SUBSCRIPTION_SCOPE], + ) + + def create_subscription_filter( + self, + subscription_id: str, + payload: CreateSubscriptionFilterRequest, + ) -> dict[str, object] | None: + return self.transport.request_json( + "POST", + f"/commerce/notification/v1/subscription/{subscription_id}/filter", + scopes=[NOTIFICATION_SCOPE, NOTIFICATION_SUBSCRIPTION_SCOPE], + headers={"Content-Type": "application/json"}, + json_body=payload.model_dump(by_alias=True, exclude_none=True), + ) + + def get_subscription_filter(self, subscription_id: str, filter_id: str) -> SubscriptionFilter: + return self.transport.request_model( + SubscriptionFilter, + "GET", + f"/commerce/notification/v1/subscription/{subscription_id}/filter/{filter_id}", + scopes=[NOTIFICATION_SCOPE, NOTIFICATION_SUBSCRIPTION_READ_SCOPE], + ) + + def delete_subscription_filter(self, subscription_id: str, filter_id: str) -> None: + self.transport.request_json( + "DELETE", + f"/commerce/notification/v1/subscription/{subscription_id}/filter/{filter_id}", + scopes=[NOTIFICATION_SCOPE, NOTIFICATION_SUBSCRIPTION_SCOPE], + ) + + def disable_subscription(self, subscription_id: str) -> None: + self.transport.request_json( + "POST", + f"/commerce/notification/v1/subscription/{subscription_id}/disable", + scopes=[NOTIFICATION_SCOPE, NOTIFICATION_SUBSCRIPTION_SCOPE], + ) + + def enable_subscription(self, subscription_id: str) -> None: + self.transport.request_json( + "POST", + f"/commerce/notification/v1/subscription/{subscription_id}/enable", + scopes=[NOTIFICATION_SCOPE, NOTIFICATION_SUBSCRIPTION_SCOPE], + ) + def test_subscription(self, subscription_id: str) -> None: self.transport.request_json( "POST", diff --git a/tests/test_public_wrappers.py b/tests/test_public_wrappers.py index 430f535..3029861 100644 --- a/tests/test_public_wrappers.py +++ b/tests/test_public_wrappers.py @@ -14,9 +14,16 @@ from ebay_client.generated.feed.models import TaskCollection from ebay_client.generated.fulfillment.models import Order from ebay_client.generated.inventory.models import InventoryItemWithSkuLocaleGroupid from ebay_client.generated.notification.models import ( + Config, + CreateSubscriptionFilterRequest, + CreateSubscriptionRequest, DeliveryConfig, + Subscription, + SubscriptionFilter, + SubscriptionPayloadDetail, DestinationRequest, TopicSearchResponse, + UpdateSubscriptionRequest, ) from ebay_client.inventory.client import InventoryClient from ebay_client.notification.client import NotificationClient @@ -74,6 +81,127 @@ def test_notification_wrapper_serializes_typed_request_model(httpx_mock: HTTPXMo assert body["deliveryConfig"]["endpoint"] == "https://example.test/webhook" +def test_notification_config_wrapper_round_trip(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + method="GET", + url="https://api.ebay.com/commerce/notification/v1/config", + json={"alertEmail": "alerts@example.test"}, + ) + httpx_mock.add_response( + method="PUT", + url="https://api.ebay.com/commerce/notification/v1/config", + status_code=204, + ) + + client = NotificationClient(build_transport()) + config = client.get_config() + client.update_config(Config(alertEmail="ops@example.test")) + + assert isinstance(config, Config) + assert config.alertEmail == "alerts@example.test" + request = httpx_mock.get_requests()[1] + body = json.loads(request.content.decode("utf-8")) + assert body["alertEmail"] == "ops@example.test" + + +def test_notification_subscription_wrapper_serializes_and_returns_models(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + method="POST", + url="https://api.ebay.com/commerce/notification/v1/subscription", + json={}, + status_code=201, + ) + httpx_mock.add_response( + method="GET", + url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1", + json={"subscriptionId": "SUB-1", "topicId": "TOPIC-1", "status": "ENABLED"}, + ) + httpx_mock.add_response( + method="PUT", + url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1", + status_code=204, + ) + httpx_mock.add_response( + method="POST", + url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1/enable", + status_code=204, + ) + httpx_mock.add_response( + method="POST", + url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1/test", + status_code=202, + ) + + client = NotificationClient(build_transport()) + create_payload = CreateSubscriptionRequest( + destinationId="DEST-1", + topicId="TOPIC-1", + status="DISABLED", + payload=SubscriptionPayloadDetail( + deliveryProtocol="HTTPS", + format="JSON", + schemaVersion="1.0", + ), + ) + update_payload = UpdateSubscriptionRequest(status="ENABLED", destinationId="DEST-1") + + client.create_subscription(create_payload) + subscription = client.get_subscription("SUB-1") + client.update_subscription("SUB-1", update_payload) + client.enable_subscription("SUB-1") + client.test_subscription("SUB-1") + + assert isinstance(subscription, Subscription) + assert subscription.subscriptionId == "SUB-1" + create_request = httpx_mock.get_requests()[0] + create_body = json.loads(create_request.content.decode("utf-8")) + assert create_body["destinationId"] == "DEST-1" + assert create_body["payload"]["deliveryProtocol"] == "HTTPS" + update_request = httpx_mock.get_requests()[2] + update_body = json.loads(update_request.content.decode("utf-8")) + assert update_body == {"destinationId": "DEST-1", "status": "ENABLED"} + + +def test_notification_subscription_filter_wrapper_serializes_and_returns_model(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + method="POST", + url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1/filter", + json={}, + status_code=201, + ) + httpx_mock.add_response( + method="GET", + url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1/filter/FILTER-1", + json={"filterId": "FILTER-1", "subscriptionId": "SUB-1", "filterStatus": "ENABLED"}, + ) + httpx_mock.add_response( + method="DELETE", + url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1/filter/FILTER-1", + status_code=204, + ) + + client = NotificationClient(build_transport()) + payload = CreateSubscriptionFilterRequest( + filterSchema={ + "properties": { + "data": { + "type": "object", + } + } + } + ) + + client.create_subscription_filter("SUB-1", payload) + subscription_filter = client.get_subscription_filter("SUB-1", "FILTER-1") + client.delete_subscription_filter("SUB-1", "FILTER-1") + + assert isinstance(subscription_filter, SubscriptionFilter) + assert subscription_filter.filterId == "FILTER-1" + request = httpx_mock.get_requests()[0] + body = json.loads(request.content.decode("utf-8")) + assert body["filterSchema"]["properties"]["data"]["type"] == "object" + + def test_inventory_wrapper_returns_inventory_item_model(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( method="GET",