Add media upload convenience methods and example workflows for images, documents, and videos
This commit is contained in:
parent
ee539e8189
commit
937cd86c8b
4 changed files with 325 additions and 2 deletions
10
README.md
10
README.md
|
|
@ -16,6 +16,16 @@ The Media wrapper includes workflow helpers on top of the raw endpoints:
|
|||
- `extract_resource_id()` to pull a media resource ID from a `Location` header
|
||||
- `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_document()` to stage, upload, and poll a document in one call
|
||||
- `create_document_from_url_and_wait()` to create a document from a URL and poll until it is accepted
|
||||
|
||||
A concrete workflow example is available in `examples/media_workflows.py` for:
|
||||
|
||||
- uploading an image from a file
|
||||
- creating an image from a URL
|
||||
- staging, uploading, and polling a document
|
||||
- staging, uploading, and polling a video
|
||||
|
||||
## Generate Low-Level Clients
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,30 @@ class MediaClient:
|
|||
location = response.headers.get("Location")
|
||||
return CreatedMediaResource(location=location, resource_id=extract_resource_id(location))
|
||||
|
||||
def create_upload_and_wait_video(
|
||||
self,
|
||||
payload: CreateVideoRequest,
|
||||
*,
|
||||
content: bytes,
|
||||
content_length: int | None = None,
|
||||
content_range: str | None = None,
|
||||
timeout_seconds: float = 30.0,
|
||||
poll_interval_seconds: float = 1.0,
|
||||
) -> Video:
|
||||
created = self.create_video(payload)
|
||||
video_id = self._require_resource_id(created.resource_id, "video resource ID")
|
||||
self.upload_video(
|
||||
video_id,
|
||||
content=content,
|
||||
content_length=content_length if content_length is not None else len(content),
|
||||
content_range=content_range,
|
||||
)
|
||||
return self.wait_for_video(
|
||||
video_id,
|
||||
timeout_seconds=timeout_seconds,
|
||||
poll_interval_seconds=poll_interval_seconds,
|
||||
)
|
||||
|
||||
def get_video(self, video_id: str) -> Video:
|
||||
return self.transport.request_model(
|
||||
Video,
|
||||
|
|
@ -125,6 +149,30 @@ class MediaClient:
|
|||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def create_upload_and_wait_document(
|
||||
self,
|
||||
payload: CreateDocumentRequest,
|
||||
*,
|
||||
file_name: str,
|
||||
content: bytes,
|
||||
content_type: str = "application/octet-stream",
|
||||
timeout_seconds: float = 30.0,
|
||||
poll_interval_seconds: float = 1.0,
|
||||
) -> DocumentResponse:
|
||||
created = self.create_document(payload)
|
||||
document_id = self._require_resource_id(created.documentId, "documentId")
|
||||
self.upload_document(
|
||||
document_id,
|
||||
file_name=file_name,
|
||||
content=content,
|
||||
content_type=content_type,
|
||||
)
|
||||
return self.wait_for_document(
|
||||
document_id,
|
||||
timeout_seconds=timeout_seconds,
|
||||
poll_interval_seconds=poll_interval_seconds,
|
||||
)
|
||||
|
||||
def create_document_from_url(self, payload: CreateDocumentFromUrlRequest) -> CreateDocumentResponse:
|
||||
return self.transport.request_model(
|
||||
CreateDocumentResponse,
|
||||
|
|
@ -135,6 +183,21 @@ class MediaClient:
|
|||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def create_document_from_url_and_wait(
|
||||
self,
|
||||
payload: CreateDocumentFromUrlRequest,
|
||||
*,
|
||||
timeout_seconds: float = 30.0,
|
||||
poll_interval_seconds: float = 1.0,
|
||||
) -> DocumentResponse:
|
||||
created = self.create_document_from_url(payload)
|
||||
document_id = self._require_resource_id(created.documentId, "documentId")
|
||||
return self.wait_for_document(
|
||||
document_id,
|
||||
timeout_seconds=timeout_seconds,
|
||||
poll_interval_seconds=poll_interval_seconds,
|
||||
)
|
||||
|
||||
def get_document(self, document_id: str) -> DocumentResponse:
|
||||
return self.transport.request_model(
|
||||
DocumentResponse,
|
||||
|
|
@ -222,4 +285,10 @@ class MediaClient:
|
|||
raise ValueError(f"{resource_label} reached terminal failure status: {status}")
|
||||
if monotonic() >= deadline:
|
||||
raise TimeoutError(f"Timed out while waiting for {resource_label}; last status was {status!r}")
|
||||
sleep(poll_interval_seconds)
|
||||
sleep(poll_interval_seconds)
|
||||
|
||||
@staticmethod
|
||||
def _require_resource_id(value: str | None, field_name: str) -> str:
|
||||
if not value:
|
||||
raise RuntimeError(f"eBay did not return a required {field_name}")
|
||||
return value
|
||||
86
examples/media_workflows.py
Normal file
86
examples/media_workflows.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from ebay_client.client import EbayClient
|
||||
from ebay_client.core.auth.models import EbayOAuthConfig
|
||||
from ebay_client.generated.media.models import (
|
||||
CreateDocumentRequest,
|
||||
CreateImageFromUrlRequest,
|
||||
CreateVideoRequest,
|
||||
)
|
||||
|
||||
|
||||
def build_client() -> EbayClient:
|
||||
oauth_config = EbayOAuthConfig(
|
||||
client_id=os.environ["EBAY_CLIENT_ID"],
|
||||
client_secret=os.environ["EBAY_CLIENT_SECRET"],
|
||||
default_scopes=["https://api.ebay.com/oauth/api_scope/sell.inventory"],
|
||||
)
|
||||
return EbayClient(oauth_config)
|
||||
|
||||
|
||||
def upload_image_from_file(client: EbayClient, image_path: Path) -> None:
|
||||
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)
|
||||
|
||||
|
||||
def upload_image_from_url(client: EbayClient, image_url: str) -> None:
|
||||
image = client.media.create_image_from_url(CreateImageFromUrlRequest(imageUrl=image_url))
|
||||
print("image_url:", image.imageUrl)
|
||||
|
||||
|
||||
def upload_document_and_wait(client: EbayClient, document_path: Path) -> None:
|
||||
accepted = client.media.create_upload_and_wait_document(
|
||||
CreateDocumentRequest(
|
||||
documentType="USER_GUIDE_OR_MANUAL",
|
||||
languages=["en-US"],
|
||||
),
|
||||
file_name=document_path.name,
|
||||
content=document_path.read_bytes(),
|
||||
content_type="application/pdf",
|
||||
timeout_seconds=60.0,
|
||||
)
|
||||
print("document_final_status:", accepted.documentStatus)
|
||||
|
||||
|
||||
def upload_video_and_wait(client: EbayClient, video_path: Path) -> None:
|
||||
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_status:", live_video.status)
|
||||
print("video_id:", live_video.videoId)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
client = build_client()
|
||||
|
||||
image_file = os.environ.get("EBAY_MEDIA_IMAGE_FILE")
|
||||
image_url = os.environ.get("EBAY_MEDIA_IMAGE_URL")
|
||||
document_file = os.environ.get("EBAY_MEDIA_DOCUMENT_FILE")
|
||||
video_file = os.environ.get("EBAY_MEDIA_VIDEO_FILE")
|
||||
|
||||
if image_file:
|
||||
upload_image_from_file(client, Path(image_file))
|
||||
if image_url:
|
||||
upload_image_from_url(client, image_url)
|
||||
if document_file:
|
||||
upload_document_and_wait(client, Path(document_file))
|
||||
if video_file:
|
||||
upload_video_and_wait(client, Path(video_file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -16,6 +16,7 @@ from ebay_client.generated.inventory.models import InventoryItemWithSkuLocaleGro
|
|||
from ebay_client.generated.media.models import (
|
||||
CreateDocumentFromUrlRequest,
|
||||
CreateDocumentRequest,
|
||||
CreateDocumentResponse,
|
||||
CreateImageFromUrlRequest,
|
||||
CreateVideoRequest,
|
||||
DocumentResponse,
|
||||
|
|
@ -562,4 +563,161 @@ def test_media_wait_for_document_raises_on_terminal_failure(monkeypatch) -> None
|
|||
except ValueError as exc:
|
||||
assert "REJECTED" in str(exc)
|
||||
else:
|
||||
raise AssertionError("Expected wait_for_document to raise on terminal failure status")
|
||||
raise AssertionError("Expected wait_for_document to raise on terminal failure status")
|
||||
|
||||
|
||||
def test_media_create_upload_and_wait_video_orchestrates_flow(monkeypatch) -> None:
|
||||
client = MediaClient(build_transport())
|
||||
calls: list[tuple[str, object]] = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
client,
|
||||
"create_video",
|
||||
lambda payload: calls.append(("create_video", payload)) or CreatedMediaResource(resource_id="VIDEO-9"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
client,
|
||||
"upload_video",
|
||||
lambda video_id, **kwargs: calls.append(("upload_video", {"video_id": video_id, **kwargs})),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
client,
|
||||
"wait_for_video",
|
||||
lambda video_id, **kwargs: calls.append(("wait_for_video", {"video_id": video_id, **kwargs}))
|
||||
or Video(videoId=video_id, status="LIVE"),
|
||||
)
|
||||
|
||||
result = client.create_upload_and_wait_video(
|
||||
CreateVideoRequest(title="Demo", size=4, classification=["ITEM"]),
|
||||
content=b"demo",
|
||||
poll_interval_seconds=0.0,
|
||||
)
|
||||
|
||||
assert result.videoId == "VIDEO-9"
|
||||
assert calls[0][0] == "create_video"
|
||||
assert calls[1] == (
|
||||
"upload_video",
|
||||
{"video_id": "VIDEO-9", "content": b"demo", "content_length": 4, "content_range": None},
|
||||
)
|
||||
assert calls[2] == (
|
||||
"wait_for_video",
|
||||
{"video_id": "VIDEO-9", "timeout_seconds": 30.0, "poll_interval_seconds": 0.0},
|
||||
)
|
||||
|
||||
|
||||
def test_media_create_upload_and_wait_document_orchestrates_flow(monkeypatch) -> None:
|
||||
client = MediaClient(build_transport())
|
||||
calls: list[tuple[str, object]] = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
client,
|
||||
"create_document",
|
||||
lambda payload: calls.append(("create_document", payload))
|
||||
or CreateDocumentResponse(documentId="DOC-9", documentStatus="PENDING_UPLOAD"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
client,
|
||||
"upload_document",
|
||||
lambda document_id, **kwargs: calls.append(("upload_document", {"document_id": document_id, **kwargs}))
|
||||
or DocumentResponse(documentId=document_id, documentStatus="SUBMITTED"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
client,
|
||||
"wait_for_document",
|
||||
lambda document_id, **kwargs: calls.append(("wait_for_document", {"document_id": document_id, **kwargs}))
|
||||
or DocumentResponse(documentId=document_id, documentStatus="ACCEPTED"),
|
||||
)
|
||||
|
||||
result = client.create_upload_and_wait_document(
|
||||
CreateDocumentRequest(documentType="USER_GUIDE_OR_MANUAL", languages=["en-US"]),
|
||||
file_name="guide.pdf",
|
||||
content=b"%PDF-1.7",
|
||||
content_type="application/pdf",
|
||||
poll_interval_seconds=0.0,
|
||||
)
|
||||
|
||||
assert result.documentStatus == "ACCEPTED"
|
||||
assert calls[0][0] == "create_document"
|
||||
assert calls[1] == (
|
||||
"upload_document",
|
||||
{
|
||||
"document_id": "DOC-9",
|
||||
"file_name": "guide.pdf",
|
||||
"content": b"%PDF-1.7",
|
||||
"content_type": "application/pdf",
|
||||
},
|
||||
)
|
||||
assert calls[2] == (
|
||||
"wait_for_document",
|
||||
{"document_id": "DOC-9", "timeout_seconds": 30.0, "poll_interval_seconds": 0.0},
|
||||
)
|
||||
|
||||
|
||||
def test_media_create_document_from_url_and_wait_orchestrates_flow(monkeypatch) -> None:
|
||||
client = MediaClient(build_transport())
|
||||
calls: list[tuple[str, object]] = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
client,
|
||||
"create_document_from_url",
|
||||
lambda payload: calls.append(("create_document_from_url", payload))
|
||||
or CreateDocumentResponse(documentId="DOC-10", documentStatus="SUBMITTED"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
client,
|
||||
"wait_for_document",
|
||||
lambda document_id, **kwargs: calls.append(("wait_for_document", {"document_id": document_id, **kwargs}))
|
||||
or DocumentResponse(documentId=document_id, documentStatus="ACCEPTED"),
|
||||
)
|
||||
|
||||
result = client.create_document_from_url_and_wait(
|
||||
CreateDocumentFromUrlRequest(
|
||||
documentType="USER_GUIDE_OR_MANUAL",
|
||||
documentUrl="https://example.test/guide.pdf",
|
||||
languages=["en-US"],
|
||||
),
|
||||
poll_interval_seconds=0.0,
|
||||
)
|
||||
|
||||
assert result.documentStatus == "ACCEPTED"
|
||||
assert calls[0][0] == "create_document_from_url"
|
||||
assert calls[1] == (
|
||||
"wait_for_document",
|
||||
{"document_id": "DOC-10", "timeout_seconds": 30.0, "poll_interval_seconds": 0.0},
|
||||
)
|
||||
|
||||
|
||||
def test_media_convenience_methods_raise_when_required_ids_are_missing(monkeypatch) -> None:
|
||||
client = MediaClient(build_transport())
|
||||
|
||||
monkeypatch.setattr(client, "create_video", lambda payload: CreatedMediaResource(resource_id=None))
|
||||
monkeypatch.setattr(client, "create_document", lambda payload: CreateDocumentResponse(documentId=None))
|
||||
monkeypatch.setattr(client, "create_document_from_url", lambda payload: CreateDocumentResponse(documentId=None))
|
||||
|
||||
for action in (
|
||||
lambda: client.create_upload_and_wait_video(
|
||||
CreateVideoRequest(title="Demo", size=4, classification=["ITEM"]),
|
||||
content=b"demo",
|
||||
poll_interval_seconds=0.0,
|
||||
),
|
||||
lambda: client.create_upload_and_wait_document(
|
||||
CreateDocumentRequest(documentType="USER_GUIDE_OR_MANUAL", languages=["en-US"]),
|
||||
file_name="guide.pdf",
|
||||
content=b"%PDF-1.7",
|
||||
poll_interval_seconds=0.0,
|
||||
),
|
||||
lambda: client.create_document_from_url_and_wait(
|
||||
CreateDocumentFromUrlRequest(
|
||||
documentType="USER_GUIDE_OR_MANUAL",
|
||||
documentUrl="https://example.test/guide.pdf",
|
||||
languages=["en-US"],
|
||||
),
|
||||
poll_interval_seconds=0.0,
|
||||
),
|
||||
):
|
||||
try:
|
||||
action()
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("Expected convenience method to raise when eBay omits the required identifier")
|
||||
Loading…
Add table
Add a link
Reference in a new issue