diff --git a/README.md b/README.md index ebc471a..30a6d75 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,12 @@ Currently wired API domains include Notification, Inventory, Fulfillment, Accoun The Media wrapper includes workflow helpers on top of the raw endpoints: -- `VideoWorkflowResult` and `DocumentWorkflowResult` to return structured data from the higher-level workflows - `extract_resource_id()` to pull a media resource ID from a `Location` header -- `guess_media_content_type()` to infer a content type from a file name when possible - `wait_for_video()` to poll until a video reaches `LIVE` or a terminal failure state - `wait_for_document()` to poll until a document reaches `ACCEPTED` or a terminal failure state - `create_upload_and_wait_video()` to stage, upload, and poll a video in one call -- `create_upload_and_wait_video_from_path()` to do the same directly from a local file path - `create_upload_and_wait_document()` to stage, upload, and poll a document in one call -- `create_upload_and_wait_document_from_path()` to do the same directly from a local file path - `create_document_from_url_and_wait()` to create a document from a URL and poll until it is accepted -- `create_image_from_path()` and `upload_document_from_path()` for path-based local file uploads A concrete workflow example is available in `examples/media_workflows.py` for: diff --git a/ebay_client/account/client.py b/ebay_client/account/client.py index 9202d97..19e7abf 100644 --- a/ebay_client/account/client.py +++ b/ebay_client/account/client.py @@ -2,19 +2,10 @@ from __future__ import annotations from ebay_client.core.http.transport import ApiTransport from ebay_client.generated.account.models import ( - FulfillmentPolicy, - FulfillmentPolicyRequest, FulfillmentPolicyResponse, - PaymentPolicy, - PaymentPolicyRequest, PaymentPolicyResponse, Programs, - ReturnPolicy, - ReturnPolicyRequest, ReturnPolicyResponse, - SetFulfillmentPolicyResponse, - SetPaymentPolicyResponse, - SetReturnPolicyResponse, SellingPrivileges, ) @@ -36,54 +27,6 @@ class AccountClient: params={"marketplace_id": marketplace_id}, ) - def create_fulfillment_policy(self, payload: FulfillmentPolicyRequest) -> SetFulfillmentPolicyResponse: - return self.transport.request_model( - SetFulfillmentPolicyResponse, - "POST", - "/sell/account/v1/fulfillment_policy/", - scopes=[ACCOUNT_SCOPE], - headers={"Content-Type": "application/json"}, - json_body=payload.model_dump(by_alias=True, exclude_none=True), - ) - - def get_fulfillment_policy(self, fulfillment_policy_id: str) -> FulfillmentPolicy: - return self.transport.request_model( - FulfillmentPolicy, - "GET", - f"/sell/account/v1/fulfillment_policy/{fulfillment_policy_id}", - scope_options=ACCOUNT_READ_SCOPE_OPTIONS, - ) - - def update_fulfillment_policy( - self, - fulfillment_policy_id: str, - payload: FulfillmentPolicyRequest, - ) -> SetFulfillmentPolicyResponse: - return self.transport.request_model( - SetFulfillmentPolicyResponse, - "PUT", - f"/sell/account/v1/fulfillment_policy/{fulfillment_policy_id}", - scopes=[ACCOUNT_SCOPE], - headers={"Content-Type": "application/json"}, - json_body=payload.model_dump(by_alias=True, exclude_none=True), - ) - - def delete_fulfillment_policy(self, fulfillment_policy_id: str) -> None: - self.transport.request_json( - "DELETE", - f"/sell/account/v1/fulfillment_policy/{fulfillment_policy_id}", - scopes=[ACCOUNT_SCOPE], - ) - - def get_fulfillment_policy_by_name(self, *, marketplace_id: str, name: str) -> FulfillmentPolicy: - return self.transport.request_model( - FulfillmentPolicy, - "GET", - "/sell/account/v1/fulfillment_policy/get_by_policy_name", - scope_options=ACCOUNT_READ_SCOPE_OPTIONS, - params={"marketplace_id": marketplace_id, "name": name}, - ) - def get_payment_policies(self, *, marketplace_id: str) -> PaymentPolicyResponse: return self.transport.request_model( PaymentPolicyResponse, @@ -93,50 +36,6 @@ class AccountClient: params={"marketplace_id": marketplace_id}, ) - def create_payment_policy(self, payload: PaymentPolicyRequest) -> SetPaymentPolicyResponse: - return self.transport.request_model( - SetPaymentPolicyResponse, - "POST", - "/sell/account/v1/payment_policy", - scopes=[ACCOUNT_SCOPE], - headers={"Content-Type": "application/json"}, - json_body=payload.model_dump(by_alias=True, exclude_none=True), - ) - - def get_payment_policy(self, payment_policy_id: str) -> PaymentPolicy: - return self.transport.request_model( - PaymentPolicy, - "GET", - f"/sell/account/v1/payment_policy/{payment_policy_id}", - scope_options=ACCOUNT_READ_SCOPE_OPTIONS, - ) - - def update_payment_policy(self, payment_policy_id: str, payload: PaymentPolicyRequest) -> SetPaymentPolicyResponse: - return self.transport.request_model( - SetPaymentPolicyResponse, - "PUT", - f"/sell/account/v1/payment_policy/{payment_policy_id}", - scopes=[ACCOUNT_SCOPE], - headers={"Content-Type": "application/json"}, - json_body=payload.model_dump(by_alias=True, exclude_none=True), - ) - - def delete_payment_policy(self, payment_policy_id: str) -> None: - self.transport.request_json( - "DELETE", - f"/sell/account/v1/payment_policy/{payment_policy_id}", - scopes=[ACCOUNT_SCOPE], - ) - - def get_payment_policy_by_name(self, *, marketplace_id: str, name: str) -> PaymentPolicy: - return self.transport.request_model( - PaymentPolicy, - "GET", - "/sell/account/v1/payment_policy/get_by_policy_name", - scope_options=ACCOUNT_READ_SCOPE_OPTIONS, - params={"marketplace_id": marketplace_id, "name": name}, - ) - def get_return_policies(self, *, marketplace_id: str) -> ReturnPolicyResponse: return self.transport.request_model( ReturnPolicyResponse, @@ -146,50 +45,6 @@ class AccountClient: params={"marketplace_id": marketplace_id}, ) - def create_return_policy(self, payload: ReturnPolicyRequest) -> SetReturnPolicyResponse: - return self.transport.request_model( - SetReturnPolicyResponse, - "POST", - "/sell/account/v1/return_policy", - scopes=[ACCOUNT_SCOPE], - headers={"Content-Type": "application/json"}, - json_body=payload.model_dump(by_alias=True, exclude_none=True), - ) - - def get_return_policy(self, return_policy_id: str) -> ReturnPolicy: - return self.transport.request_model( - ReturnPolicy, - "GET", - f"/sell/account/v1/return_policy/{return_policy_id}", - scope_options=ACCOUNT_READ_SCOPE_OPTIONS, - ) - - def update_return_policy(self, return_policy_id: str, payload: ReturnPolicyRequest) -> SetReturnPolicyResponse: - return self.transport.request_model( - SetReturnPolicyResponse, - "PUT", - f"/sell/account/v1/return_policy/{return_policy_id}", - scopes=[ACCOUNT_SCOPE], - headers={"Content-Type": "application/json"}, - json_body=payload.model_dump(by_alias=True, exclude_none=True), - ) - - def delete_return_policy(self, return_policy_id: str) -> None: - self.transport.request_json( - "DELETE", - f"/sell/account/v1/return_policy/{return_policy_id}", - scopes=[ACCOUNT_SCOPE], - ) - - def get_return_policy_by_name(self, *, marketplace_id: str, name: str) -> ReturnPolicy: - return self.transport.request_model( - ReturnPolicy, - "GET", - "/sell/account/v1/return_policy/get_by_policy_name", - scope_options=ACCOUNT_READ_SCOPE_OPTIONS, - params={"marketplace_id": marketplace_id, "name": name}, - ) - def get_privileges(self) -> SellingPrivileges: return self.transport.request_model( SellingPrivileges, diff --git a/ebay_client/feed/client.py b/ebay_client/feed/client.py index 7667ac0..2ed0db6 100644 --- a/ebay_client/feed/client.py +++ b/ebay_client/feed/client.py @@ -1,20 +1,11 @@ from __future__ import annotations -from urllib.parse import urlparse - -from pydantic import BaseModel - from ebay_client.core.http.transport import ApiTransport from ebay_client.generated.feed.models import ( - CreateTaskRequest, - CreateUserScheduleRequest, - ScheduleTemplateResponse, ScheduleTemplateCollection, Task, TaskCollection, - UserScheduleResponse, UserScheduleCollection, - UpdateUserScheduleRequest, ) FEED_INVENTORY_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.inventory" @@ -31,96 +22,19 @@ FEED_READ_SCOPE_OPTIONS = [ ] -class CreatedFeedResource(BaseModel): - location: str | None = None - resource_id: str | None = None - - -class FeedFileDownload(BaseModel): - content: bytes - content_disposition: str | None = None - file_name: str | None = None - - -def extract_feed_resource_id(location: str | None) -> str | None: - if not location: - return None - - path = urlparse(location).path.rstrip("/") - if not path: - return None - - _, _, resource_id = path.rpartition("/") - return resource_id or None - - -def _extract_file_name(content_disposition: str | None) -> str | None: - if not content_disposition: - return None - - marker = "filename=" - if marker not in content_disposition: - return None - - value = content_disposition.split(marker, 1)[1].strip() - if value.startswith('"') and value.endswith('"'): - return value[1:-1] - return value - - class FeedClient: def __init__(self, transport: ApiTransport) -> None: self.transport = transport - def get_tasks( - self, - *, - feed_type: str | None = None, - schedule_id: str | None = None, - date_range: str | None = None, - look_back_days: int | None = None, - limit: int | None = None, - offset: int | None = None, - ) -> TaskCollection: + def get_tasks(self, *, feed_type: str | None = None) -> TaskCollection: return self.transport.request_model( TaskCollection, "GET", "/sell/feed/v1/task", scope_options=FEED_READ_SCOPE_OPTIONS, - params={ - "feed_type": feed_type, - "schedule_id": schedule_id, - "date_range": date_range, - "look_back_days": look_back_days, - "limit": limit, - "offset": offset, - }, + params={"feed_type": feed_type}, ) - def create_task( - self, - payload: CreateTaskRequest, - *, - marketplace_id: str, - accept_language: str | None = None, - ) -> CreatedFeedResource: - headers = { - "Content-Type": "application/json", - "X-EBAY-C-MARKETPLACE-ID": marketplace_id, - } - if accept_language is not None: - headers["Accept-Language"] = accept_language - - response = self.transport.request( - "POST", - "/sell/feed/v1/task", - scope_options=FEED_READ_SCOPE_OPTIONS, - headers=headers, - json_body=payload.model_dump(by_alias=True, exclude_none=True), - ) - location = response.headers.get("Location") - return CreatedFeedResource(location=location, resource_id=extract_feed_resource_id(location)) - def get_task(self, task_id: str) -> Task: return self.transport.request_model( Task, @@ -129,108 +43,18 @@ class FeedClient: scope_options=FEED_READ_SCOPE_OPTIONS, ) - def get_input_file(self, task_id: str) -> FeedFileDownload: - return self._download_file(f"/sell/feed/v1/task/{task_id}/download_input_file") - - def get_result_file(self, task_id: str) -> FeedFileDownload: - return self._download_file(f"/sell/feed/v1/task/{task_id}/download_result_file") - - def upload_file( - self, - task_id: str, - *, - file_name: str, - content: bytes, - content_type: str = "application/octet-stream", - ) -> None: - self.transport.request_json( - "POST", - f"/sell/feed/v1/task/{task_id}/upload_file", - scope_options=FEED_READ_SCOPE_OPTIONS, - files={"file": (file_name, content, content_type)}, - ) - - def get_schedule_templates( - self, - *, - feed_type: str, - limit: int | None = None, - offset: int | None = None, - ) -> ScheduleTemplateCollection: + def get_schedule_templates(self) -> ScheduleTemplateCollection: return self.transport.request_model( ScheduleTemplateCollection, "GET", "/sell/feed/v1/schedule_template", scope_options=FEED_READ_SCOPE_OPTIONS, - params={"feed_type": feed_type, "limit": limit, "offset": offset}, ) - def get_schedule_template(self, schedule_template_id: str) -> ScheduleTemplateResponse: - return self.transport.request_model( - ScheduleTemplateResponse, - "GET", - f"/sell/feed/v1/schedule_template/{schedule_template_id}", - scope_options=FEED_READ_SCOPE_OPTIONS, - ) - - def get_schedules( - self, - *, - feed_type: str, - limit: int | None = None, - offset: int | None = None, - ) -> UserScheduleCollection: + def get_schedules(self) -> UserScheduleCollection: return self.transport.request_model( UserScheduleCollection, "GET", "/sell/feed/v1/schedule", scope_options=FEED_READ_SCOPE_OPTIONS, - params={"feed_type": feed_type, "limit": limit, "offset": offset}, - ) - - def create_schedule(self, payload: CreateUserScheduleRequest) -> CreatedFeedResource: - response = self.transport.request( - "POST", - "/sell/feed/v1/schedule", - scope_options=FEED_READ_SCOPE_OPTIONS, - headers={"Content-Type": "application/json"}, - json_body=payload.model_dump(by_alias=True, exclude_none=True), - ) - location = response.headers.get("Location") - return CreatedFeedResource(location=location, resource_id=extract_feed_resource_id(location)) - - def get_schedule(self, schedule_id: str) -> UserScheduleResponse: - return self.transport.request_model( - UserScheduleResponse, - "GET", - f"/sell/feed/v1/schedule/{schedule_id}", - scope_options=FEED_READ_SCOPE_OPTIONS, - ) - - def update_schedule(self, schedule_id: str, payload: UpdateUserScheduleRequest) -> None: - self.transport.request_json( - "PUT", - f"/sell/feed/v1/schedule/{schedule_id}", - scope_options=FEED_READ_SCOPE_OPTIONS, - headers={"Content-Type": "application/json"}, - json_body=payload.model_dump(by_alias=True, exclude_none=True), - ) - - def delete_schedule(self, schedule_id: str) -> None: - self.transport.request_json( - "DELETE", - f"/sell/feed/v1/schedule/{schedule_id}", - scope_options=FEED_READ_SCOPE_OPTIONS, - ) - - def get_latest_result_file(self, schedule_id: str) -> FeedFileDownload: - return self._download_file(f"/sell/feed/v1/schedule/{schedule_id}/download_result_file") - - def _download_file(self, path: str) -> FeedFileDownload: - response = self.transport.request("GET", path, scope_options=FEED_READ_SCOPE_OPTIONS) - content_disposition = response.headers.get("content-disposition") - return FeedFileDownload( - content=response.content, - content_disposition=content_disposition, - file_name=_extract_file_name(content_disposition), ) diff --git a/ebay_client/media/__init__.py b/ebay_client/media/__init__.py index 472fb01..1288b1e 100644 --- a/ebay_client/media/__init__.py +++ b/ebay_client/media/__init__.py @@ -1,17 +1,3 @@ -from ebay_client.media.client import ( - CreatedMediaResource, - DocumentWorkflowResult, - MediaClient, - VideoWorkflowResult, - extract_resource_id, - guess_media_content_type, -) +from ebay_client.media.client import CreatedMediaResource, MediaClient, extract_resource_id -__all__ = [ - "CreatedMediaResource", - "DocumentWorkflowResult", - "MediaClient", - "VideoWorkflowResult", - "extract_resource_id", - "guess_media_content_type", -] \ No newline at end of file +__all__ = ["CreatedMediaResource", "MediaClient", "extract_resource_id"] \ No newline at end of file diff --git a/ebay_client/media/client.py b/ebay_client/media/client.py index 5d0e003..1d82810 100644 --- a/ebay_client/media/client.py +++ b/ebay_client/media/client.py @@ -1,7 +1,5 @@ from __future__ import annotations -import mimetypes -from pathlib import Path from time import monotonic, sleep from urllib.parse import urlparse @@ -27,19 +25,6 @@ class CreatedMediaResource(BaseModel): resource_id: str | None = None -class VideoWorkflowResult(BaseModel): - created: CreatedMediaResource - video: Video - video_id: str | None = None - - -class DocumentWorkflowResult(BaseModel): - created: CreateDocumentResponse - uploaded: DocumentResponse | None = None - document: DocumentResponse - document_id: str | None = None - - def extract_resource_id(location: str | None) -> str | None: if not location: return None @@ -52,11 +37,6 @@ def extract_resource_id(location: str | None) -> str | None: return resource_id or None -def guess_media_content_type(file_name: str, *, default: str = "application/octet-stream") -> str: - guessed_type, _ = mimetypes.guess_type(file_name) - return guessed_type or default - - class MediaClient: def __init__(self, transport: ApiTransport) -> None: self.transport = transport @@ -76,14 +56,6 @@ class MediaClient: files={"image": (file_name, content, content_type)}, ) - def create_image_from_path(self, image_path: str | Path) -> ImageResponse: - path = Path(image_path) - return self.create_image_from_file( - file_name=path.name, - content=path.read_bytes(), - content_type=guess_media_content_type(path.name), - ) - def create_image_from_url(self, payload: CreateImageFromUrlRequest) -> ImageResponse: return self.transport.request_model( ImageResponse, @@ -122,7 +94,7 @@ class MediaClient: content_range: str | None = None, timeout_seconds: float = 30.0, poll_interval_seconds: float = 1.0, - ) -> VideoWorkflowResult: + ) -> Video: created = self.create_video(payload) video_id = self._require_resource_id(created.resource_id, "video resource ID") self.upload_video( @@ -131,37 +103,11 @@ class MediaClient: content_length=content_length if content_length is not None else len(content), content_range=content_range, ) - video = self.wait_for_video( + return self.wait_for_video( video_id, timeout_seconds=timeout_seconds, poll_interval_seconds=poll_interval_seconds, ) - return VideoWorkflowResult(created=created, video=video, video_id=video_id) - - def create_upload_and_wait_video_from_path( - self, - video_path: str | Path, - *, - title: str | None = None, - classification: list[str] | None = None, - description: str | None = None, - timeout_seconds: float = 30.0, - poll_interval_seconds: float = 1.0, - ) -> VideoWorkflowResult: - path = Path(video_path) - content = path.read_bytes() - payload = CreateVideoRequest( - title=title or path.stem, - size=len(content), - classification=classification or ["ITEM"], - description=description, - ) - return self.create_upload_and_wait_video( - payload, - content=content, - timeout_seconds=timeout_seconds, - poll_interval_seconds=poll_interval_seconds, - ) def get_video(self, video_id: str) -> Video: return self.transport.request_model( @@ -212,26 +158,20 @@ class MediaClient: content_type: str = "application/octet-stream", timeout_seconds: float = 30.0, poll_interval_seconds: float = 1.0, - ) -> DocumentWorkflowResult: + ) -> DocumentResponse: created = self.create_document(payload) document_id = self._require_resource_id(created.documentId, "documentId") - uploaded = self.upload_document( + self.upload_document( document_id, file_name=file_name, content=content, content_type=content_type, ) - document = self.wait_for_document( + return self.wait_for_document( document_id, timeout_seconds=timeout_seconds, poll_interval_seconds=poll_interval_seconds, ) - return DocumentWorkflowResult( - created=created, - uploaded=uploaded, - document=document, - document_id=document_id, - ) def create_document_from_url(self, payload: CreateDocumentFromUrlRequest) -> CreateDocumentResponse: return self.transport.request_model( @@ -249,20 +189,14 @@ class MediaClient: *, timeout_seconds: float = 30.0, poll_interval_seconds: float = 1.0, - ) -> DocumentWorkflowResult: + ) -> DocumentResponse: created = self.create_document_from_url(payload) document_id = self._require_resource_id(created.documentId, "documentId") - document = self.wait_for_document( + return self.wait_for_document( document_id, timeout_seconds=timeout_seconds, poll_interval_seconds=poll_interval_seconds, ) - return DocumentWorkflowResult( - created=created, - uploaded=None, - document=document, - document_id=document_id, - ) def get_document(self, document_id: str) -> DocumentResponse: return self.transport.request_model( @@ -288,33 +222,6 @@ class MediaClient: files={"file": (file_name, content, content_type)}, ) - def upload_document_from_path(self, document_id: str, document_path: str | Path) -> DocumentResponse: - path = Path(document_path) - return self.upload_document( - document_id, - file_name=path.name, - content=path.read_bytes(), - content_type=guess_media_content_type(path.name), - ) - - def create_upload_and_wait_document_from_path( - self, - payload: CreateDocumentRequest, - document_path: str | Path, - *, - timeout_seconds: float = 30.0, - poll_interval_seconds: float = 1.0, - ) -> DocumentWorkflowResult: - path = Path(document_path) - return self.create_upload_and_wait_document( - payload, - file_name=path.name, - content=path.read_bytes(), - content_type=guess_media_content_type(path.name), - timeout_seconds=timeout_seconds, - poll_interval_seconds=poll_interval_seconds, - ) - def wait_for_video( self, video_id: str, diff --git a/examples/media_workflows.py b/examples/media_workflows.py index bbc3f0d..777b4c2 100644 --- a/examples/media_workflows.py +++ b/examples/media_workflows.py @@ -22,7 +22,11 @@ def build_client() -> EbayClient: def upload_image_from_file(client: EbayClient, image_path: Path) -> None: - image = client.media.create_image_from_path(image_path) + image = client.media.create_image_from_file( + file_name=image_path.name, + content=image_path.read_bytes(), + content_type="image/jpeg", + ) print("image_url:", image.imageUrl) @@ -32,7 +36,7 @@ def upload_image_from_url(client: EbayClient, image_url: str) -> None: def upload_document_and_wait(client: EbayClient, document_path: Path) -> None: - result = client.media.create_upload_and_wait_document( + accepted = client.media.create_upload_and_wait_document( CreateDocumentRequest( documentType="USER_GUIDE_OR_MANUAL", languages=["en-US"], @@ -42,31 +46,22 @@ def upload_document_and_wait(client: EbayClient, document_path: Path) -> None: content_type="application/pdf", timeout_seconds=60.0, ) - print("document_id:", result.document_id) - print("document_final_status:", result.document.documentStatus) - - -def upload_document_and_wait_from_path(client: EbayClient, document_path: Path) -> None: - result = client.media.create_upload_and_wait_document_from_path( - CreateDocumentRequest( - documentType="USER_GUIDE_OR_MANUAL", - languages=["en-US"], - ), - document_path, - timeout_seconds=60.0, - ) - print("document_id:", result.document_id) - print("document_final_status:", result.document.documentStatus) + print("document_final_status:", accepted.documentStatus) def upload_video_and_wait(client: EbayClient, video_path: Path) -> None: - result = client.media.create_upload_and_wait_video_from_path( - video_path, - description="Example upload from the ebay-rest-client workspace.", + live_video = client.media.create_upload_and_wait_video( + CreateVideoRequest( + title=video_path.stem, + size=video_path.stat().st_size, + classification=["ITEM"], + description="Example upload from the ebay-rest-client workspace.", + ), + content=video_path.read_bytes(), timeout_seconds=120.0, ) - print("video_id:", result.video_id) - print("video_status:", result.video.status) + print("video_status:", live_video.status) + print("video_id:", live_video.videoId) def main() -> None: @@ -82,7 +77,7 @@ def main() -> None: if image_url: upload_image_from_url(client, image_url) if document_file: - upload_document_and_wait_from_path(client, Path(document_file)) + upload_document_and_wait(client, Path(document_file)) if video_file: upload_video_and_wait(client, Path(video_file)) diff --git a/tests/test_public_wrappers.py b/tests/test_public_wrappers.py index 6aec0a0..9dcaf85 100644 --- a/tests/test_public_wrappers.py +++ b/tests/test_public_wrappers.py @@ -7,25 +7,10 @@ from pytest_httpx import HTTPXMock from ebay_client.account.client import AccountClient from ebay_client.core.auth.models import OAuthToken from ebay_client.core.http.transport import ApiTransport -from ebay_client.feed.client import CreatedFeedResource, FeedClient, FeedFileDownload +from ebay_client.feed.client import FeedClient from ebay_client.fulfillment.client import FulfillmentClient -from ebay_client.generated.account.models import ( - FulfillmentPolicy, - FulfillmentPolicyRequest, - PaymentPolicy, - PaymentPolicyRequest, - Programs, - ReturnPolicy, - ReturnPolicyRequest, -) +from ebay_client.generated.account.models import Programs from ebay_client.generated.feed.models import TaskCollection -from ebay_client.generated.feed.models import ( - CreateTaskRequest, - CreateUserScheduleRequest, - ScheduleTemplateResponse, - UpdateUserScheduleRequest, - UserScheduleResponse, -) from ebay_client.generated.fulfillment.models import Order from ebay_client.generated.inventory.models import InventoryItemWithSkuLocaleGroupid from ebay_client.generated.media.models import ( @@ -51,14 +36,7 @@ from ebay_client.generated.notification.models import ( UpdateSubscriptionRequest, ) from ebay_client.inventory.client import InventoryClient -from ebay_client.media.client import ( - CreatedMediaResource, - DocumentWorkflowResult, - MediaClient, - VideoWorkflowResult, - extract_resource_id, - guess_media_content_type, -) +from ebay_client.media.client import CreatedMediaResource, MediaClient, extract_resource_id from ebay_client.notification.client import NotificationClient @@ -305,99 +283,6 @@ def test_account_wrapper_returns_programs_model(httpx_mock: HTTPXMock) -> None: assert result.programs and result.programs[0].programType == "OUT_OF_STOCK_CONTROL" -def test_account_wrapper_returns_policy_models_by_id_and_name(httpx_mock: HTTPXMock) -> None: - httpx_mock.add_response( - method="GET", - url="https://api.ebay.com/sell/account/v1/fulfillment_policy/FULFILL-1", - json={"fulfillmentPolicyId": "FULFILL-1", "name": "Fast shipping"}, - ) - httpx_mock.add_response( - method="GET", - url="https://api.ebay.com/sell/account/v1/payment_policy/get_by_policy_name?marketplace_id=EBAY_US&name=Default%20payment", - json={"paymentPolicyId": "PAY-1", "name": "Default payment"}, - ) - httpx_mock.add_response( - method="GET", - url="https://api.ebay.com/sell/account/v1/return_policy/get_by_policy_name?marketplace_id=EBAY_US&name=30%20day%20returns", - json={"returnPolicyId": "RET-1", "name": "30 day returns"}, - ) - - client = AccountClient(build_transport()) - fulfillment = client.get_fulfillment_policy("FULFILL-1") - payment = client.get_payment_policy_by_name(marketplace_id="EBAY_US", name="Default payment") - returns = client.get_return_policy_by_name(marketplace_id="EBAY_US", name="30 day returns") - - assert isinstance(fulfillment, FulfillmentPolicy) - assert fulfillment.fulfillmentPolicyId == "FULFILL-1" - assert isinstance(payment, PaymentPolicy) - assert payment.paymentPolicyId == "PAY-1" - assert isinstance(returns, ReturnPolicy) - assert returns.returnPolicyId == "RET-1" - - -def test_account_wrapper_serializes_policy_requests_and_delete(httpx_mock: HTTPXMock) -> None: - httpx_mock.add_response( - method="POST", - url="https://api.ebay.com/sell/account/v1/payment_policy", - json={"paymentPolicyId": "PAY-NEW"}, - status_code=201, - ) - httpx_mock.add_response( - method="PUT", - url="https://api.ebay.com/sell/account/v1/fulfillment_policy/FULFILL-1", - json={"fulfillmentPolicyId": "FULFILL-1"}, - status_code=200, - ) - httpx_mock.add_response( - method="DELETE", - url="https://api.ebay.com/sell/account/v1/return_policy/RET-1", - status_code=204, - ) - - client = AccountClient(build_transport()) - payment_payload = PaymentPolicyRequest(name="Default payment") - fulfillment_payload = FulfillmentPolicyRequest(name="Fast shipping") - - created_payment = client.create_payment_policy(payment_payload) - updated_fulfillment = client.update_fulfillment_policy("FULFILL-1", fulfillment_payload) - client.delete_return_policy("RET-1") - - assert created_payment.paymentPolicyId == "PAY-NEW" - assert updated_fulfillment.fulfillmentPolicyId == "FULFILL-1" - - create_request = httpx_mock.get_requests()[0] - create_body = json.loads(create_request.content.decode("utf-8")) - assert create_body["name"] == "Default payment" - - update_request = httpx_mock.get_requests()[1] - update_body = json.loads(update_request.content.decode("utf-8")) - assert update_body["name"] == "Fast shipping" - - -def test_account_wrapper_supports_return_policy_create_and_update(httpx_mock: HTTPXMock) -> None: - httpx_mock.add_response( - method="POST", - url="https://api.ebay.com/sell/account/v1/return_policy", - json={"returnPolicyId": "RET-NEW"}, - status_code=201, - ) - httpx_mock.add_response( - method="PUT", - url="https://api.ebay.com/sell/account/v1/return_policy/RET-NEW", - json={"returnPolicyId": "RET-NEW"}, - status_code=200, - ) - - client = AccountClient(build_transport()) - payload = ReturnPolicyRequest(name="30 day returns") - - created = client.create_return_policy(payload) - updated = client.update_return_policy("RET-NEW", payload) - - assert created.returnPolicyId == "RET-NEW" - assert updated.returnPolicyId == "RET-NEW" - - def test_feed_wrapper_returns_task_collection_model(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( method="GET", @@ -413,158 +298,6 @@ def test_feed_wrapper_returns_task_collection_model(httpx_mock: HTTPXMock) -> No assert result.tasks and result.tasks[0].taskId == "TASK-1" -def test_feed_wrapper_supports_schedule_crud_and_template_lookup(httpx_mock: HTTPXMock) -> None: - httpx_mock.add_response( - method="POST", - url="https://api.ebay.com/sell/feed/v1/schedule", - status_code=201, - headers={"Location": "https://api.ebay.com/sell/feed/v1/schedule/SCHEDULE-1"}, - ) - httpx_mock.add_response( - method="GET", - url="https://api.ebay.com/sell/feed/v1/schedule/SCHEDULE-1", - json={"scheduleId": "SCHEDULE-1", "feedType": "LMS_ORDER_REPORT"}, - ) - httpx_mock.add_response( - method="PUT", - url="https://api.ebay.com/sell/feed/v1/schedule/SCHEDULE-1", - status_code=204, - ) - httpx_mock.add_response( - method="DELETE", - url="https://api.ebay.com/sell/feed/v1/schedule/SCHEDULE-1", - status_code=204, - ) - httpx_mock.add_response( - method="GET", - url="https://api.ebay.com/sell/feed/v1/schedule_template/TEMPLATE-1", - json={"scheduleTemplateId": "TEMPLATE-1", "feedType": "LMS_ORDER_REPORT"}, - ) - - client = FeedClient(build_transport()) - created = client.create_schedule( - CreateUserScheduleRequest(feedType="LMS_ORDER_REPORT", scheduleTemplateId="TEMPLATE-1") - ) - schedule = client.get_schedule("SCHEDULE-1") - client.update_schedule("SCHEDULE-1", UpdateUserScheduleRequest(scheduleName="nightly")) - client.delete_schedule("SCHEDULE-1") - template = client.get_schedule_template("TEMPLATE-1") - - assert isinstance(created, CreatedFeedResource) - assert created.resource_id == "SCHEDULE-1" - assert isinstance(schedule, UserScheduleResponse) - assert schedule.scheduleId == "SCHEDULE-1" - assert isinstance(template, ScheduleTemplateResponse) - assert template.scheduleTemplateId == "TEMPLATE-1" - - create_body = json.loads(httpx_mock.get_requests()[0].content.decode("utf-8")) - assert create_body["feedType"] == "LMS_ORDER_REPORT" - update_body = json.loads(httpx_mock.get_requests()[2].content.decode("utf-8")) - assert update_body["scheduleName"] == "nightly" - - -def test_feed_wrapper_supports_task_create_upload_and_file_downloads(httpx_mock: HTTPXMock) -> None: - httpx_mock.add_response( - method="POST", - url="https://api.ebay.com/sell/feed/v1/task", - status_code=202, - headers={"Location": "https://api.ebay.com/sell/feed/v1/task/TASK-2"}, - ) - httpx_mock.add_response( - method="POST", - url="https://api.ebay.com/sell/feed/v1/task/TASK-2/upload_file", - status_code=200, - json={}, - ) - httpx_mock.add_response( - method="GET", - url="https://api.ebay.com/sell/feed/v1/task/TASK-2/download_input_file", - status_code=200, - headers={"content-disposition": 'attachment; filename="input.xml"'}, - content=b"", - ) - httpx_mock.add_response( - method="GET", - url="https://api.ebay.com/sell/feed/v1/task/TASK-2/download_result_file", - status_code=200, - headers={"content-disposition": 'attachment; filename="result.csv"'}, - content=b"id,name\n1,demo\n", - ) - httpx_mock.add_response( - method="GET", - url="https://api.ebay.com/sell/feed/v1/schedule/SCHEDULE-9/download_result_file", - status_code=200, - headers={"content-disposition": 'attachment; filename="latest.csv"'}, - content=b"id,name\n2,latest\n", - ) - - client = FeedClient(build_transport()) - created = client.create_task( - CreateTaskRequest(feedType="LMS_ORDER_REPORT", schemaVersion="1.0"), - marketplace_id="EBAY_US", - ) - client.upload_file("TASK-2", file_name="input.xml", content=b"", content_type="application/xml") - input_file = client.get_input_file("TASK-2") - result_file = client.get_result_file("TASK-2") - latest_file = client.get_latest_result_file("SCHEDULE-9") - - assert isinstance(created, CreatedFeedResource) - assert created.resource_id == "TASK-2" - assert isinstance(input_file, FeedFileDownload) - assert input_file.file_name == "input.xml" - assert input_file.content == b"" - assert result_file.file_name == "result.csv" - assert result_file.content.startswith(b"id,name") - assert latest_file.file_name == "latest.csv" - - create_request = httpx_mock.get_requests()[0] - assert create_request.headers["X-EBAY-C-MARKETPLACE-ID"] == "EBAY_US" - create_body = json.loads(create_request.content.decode("utf-8")) - assert create_body["feedType"] == "LMS_ORDER_REPORT" - - upload_request = httpx_mock.get_requests()[1] - assert upload_request.headers["Content-Type"].startswith("multipart/form-data;") - assert b"filename=\"input.xml\"" in upload_request.content - - -def test_feed_wrapper_passes_task_and_schedule_filters(httpx_mock: HTTPXMock) -> None: - httpx_mock.add_response( - method="GET", - url=( - "https://api.ebay.com/sell/feed/v1/task?feed_type=LMS_ORDER_REPORT" - "&schedule_id=SCHEDULE-1&date_range=2026-01-01T00:00:00.000Z..2026-01-02T00:00:00.000Z" - "&look_back_days=7&limit=50&offset=0" - ), - json={"tasks": [], "total": 0}, - ) - httpx_mock.add_response( - method="GET", - url="https://api.ebay.com/sell/feed/v1/schedule?feed_type=LMS_ORDER_REPORT&limit=25&offset=0", - json={"schedules": [], "total": 0}, - ) - httpx_mock.add_response( - method="GET", - url="https://api.ebay.com/sell/feed/v1/schedule_template?feed_type=LMS_ORDER_REPORT&limit=25&offset=0", - json={"scheduleTemplates": [], "total": 0}, - ) - - client = FeedClient(build_transport()) - tasks = client.get_tasks( - feed_type="LMS_ORDER_REPORT", - schedule_id="SCHEDULE-1", - date_range="2026-01-01T00:00:00.000Z..2026-01-02T00:00:00.000Z", - look_back_days=7, - limit=50, - offset=0, - ) - schedules = client.get_schedules(feed_type="LMS_ORDER_REPORT", limit=25, offset=0) - templates = client.get_schedule_templates(feed_type="LMS_ORDER_REPORT", limit=25, offset=0) - - assert tasks.total == 0 - assert schedules.total == 0 - assert templates.total == 0 - - def test_inventory_wrapper_accepts_readonly_or_full_scope_options(httpx_mock: HTTPXMock) -> None: oauth_client = RecordingOAuthClient() transport = ApiTransport(base_url="https://api.ebay.com", oauth_client=oauth_client) @@ -860,10 +593,7 @@ def test_media_create_upload_and_wait_video_orchestrates_flow(monkeypatch) -> No poll_interval_seconds=0.0, ) - assert isinstance(result, VideoWorkflowResult) - assert result.video_id == "VIDEO-9" - assert result.video.videoId == "VIDEO-9" - assert result.created.resource_id == "VIDEO-9" + assert result.videoId == "VIDEO-9" assert calls[0][0] == "create_video" assert calls[1] == ( "upload_video", @@ -906,11 +636,7 @@ def test_media_create_upload_and_wait_document_orchestrates_flow(monkeypatch) -> poll_interval_seconds=0.0, ) - assert isinstance(result, DocumentWorkflowResult) - assert result.document_id == "DOC-9" - assert result.created.documentId == "DOC-9" - assert result.uploaded is not None and result.uploaded.documentStatus == "SUBMITTED" - assert result.document.documentStatus == "ACCEPTED" + assert result.documentStatus == "ACCEPTED" assert calls[0][0] == "create_document" assert calls[1] == ( "upload_document", @@ -953,11 +679,7 @@ def test_media_create_document_from_url_and_wait_orchestrates_flow(monkeypatch) poll_interval_seconds=0.0, ) - assert isinstance(result, DocumentWorkflowResult) - assert result.document_id == "DOC-10" - assert result.created.documentId == "DOC-10" - assert result.uploaded is None - assert result.document.documentStatus == "ACCEPTED" + assert result.documentStatus == "ACCEPTED" assert calls[0][0] == "create_document_from_url" assert calls[1] == ( "wait_for_document", @@ -998,127 +720,4 @@ def test_media_convenience_methods_raise_when_required_ids_are_missing(monkeypat except RuntimeError: pass else: - raise AssertionError("Expected convenience method to raise when eBay omits the required identifier") - - -def test_guess_media_content_type_uses_filename_extension() -> None: - assert guess_media_content_type("photo.jpg") == "image/jpeg" - assert guess_media_content_type("guide.pdf") == "application/pdf" - assert guess_media_content_type("unknown.custom") == "application/octet-stream" - - -def test_media_create_image_from_path_reads_file_and_infers_content_type(tmp_path, monkeypatch) -> None: - client = MediaClient(build_transport()) - image_path = tmp_path / "photo.png" - image_path.write_bytes(b"png-data") - - captured: dict[str, object] = {} - - monkeypatch.setattr( - client, - "create_image_from_file", - lambda **kwargs: captured.update(kwargs) or ImageResponse(imageUrl="https://example.test/image"), - ) - - result = client.create_image_from_path(image_path) - - assert result.imageUrl == "https://example.test/image" - assert captured == { - "file_name": "photo.png", - "content": b"png-data", - "content_type": "image/png", - } - - -def test_media_upload_document_from_path_reads_file_and_infers_content_type(tmp_path, monkeypatch) -> None: - client = MediaClient(build_transport()) - document_path = tmp_path / "guide.pdf" - document_path.write_bytes(b"%PDF-1.7") - - captured: dict[str, object] = {} - - monkeypatch.setattr( - client, - "upload_document", - lambda document_id, **kwargs: captured.update({"document_id": document_id, **kwargs}) - or DocumentResponse(documentId=document_id, documentStatus="SUBMITTED"), - ) - - result = client.upload_document_from_path("DOC-42", document_path) - - assert result.documentId == "DOC-42" - assert captured == { - "document_id": "DOC-42", - "file_name": "guide.pdf", - "content": b"%PDF-1.7", - "content_type": "application/pdf", - } - - -def test_media_create_upload_and_wait_document_from_path_reads_file_and_delegates(tmp_path, monkeypatch) -> None: - client = MediaClient(build_transport()) - document_path = tmp_path / "guide.pdf" - document_path.write_bytes(b"%PDF-1.7") - - captured: dict[str, object] = {} - - monkeypatch.setattr( - client, - "create_upload_and_wait_document", - lambda payload, **kwargs: captured.update({"payload": payload, **kwargs}) - or DocumentWorkflowResult( - created=CreateDocumentResponse(documentId="DOC-77", documentStatus="PENDING_UPLOAD"), - uploaded=DocumentResponse(documentId="DOC-77", documentStatus="SUBMITTED"), - document=DocumentResponse(documentId="DOC-77", documentStatus="ACCEPTED"), - document_id="DOC-77", - ), - ) - - result = client.create_upload_and_wait_document_from_path( - CreateDocumentRequest(documentType="USER_GUIDE_OR_MANUAL", languages=["en-US"]), - document_path, - poll_interval_seconds=0.0, - ) - - assert isinstance(result, DocumentWorkflowResult) - assert result.document.documentStatus == "ACCEPTED" - assert captured["file_name"] == "guide.pdf" - assert captured["content"] == b"%PDF-1.7" - assert captured["content_type"] == "application/pdf" - assert captured["poll_interval_seconds"] == 0.0 - - -def test_media_create_upload_and_wait_video_from_path_builds_payload_and_delegates(tmp_path, monkeypatch) -> None: - client = MediaClient(build_transport()) - video_path = tmp_path / "demo.mp4" - video_path.write_bytes(b"video-data") - - captured: dict[str, object] = {} - - monkeypatch.setattr( - client, - "create_upload_and_wait_video", - lambda payload, **kwargs: captured.update({"payload": payload, **kwargs}) - or VideoWorkflowResult( - created=CreatedMediaResource(resource_id="VIDEO-88"), - video=Video(videoId="VIDEO-88", status="LIVE"), - video_id="VIDEO-88", - ), - ) - - result = client.create_upload_and_wait_video_from_path( - video_path, - description="demo video", - poll_interval_seconds=0.0, - ) - - assert isinstance(result, VideoWorkflowResult) - assert result.video.videoId == "VIDEO-88" - payload = captured["payload"] - assert isinstance(payload, CreateVideoRequest) - assert payload.title == "demo" - assert payload.size == len(b"video-data") - assert payload.classification == ["ITEM"] - assert payload.description == "demo video" - assert captured["content"] == b"video-data" - assert captured["poll_interval_seconds"] == 0.0 \ No newline at end of file + raise AssertionError("Expected convenience method to raise when eBay omits the required identifier") \ No newline at end of file