Add media client and models for eBay media API; implement image and video handling methods

This commit is contained in:
claudi 2026-04-07 10:12:50 +02:00
parent e99937cc43
commit 1307d5691a
9 changed files with 615 additions and 1 deletions

View file

@ -7,6 +7,8 @@ This workspace contains a Python-first eBay REST client foundation with:
- public API wrappers per eBay REST domain
- an isolated Pydantic model generation script for each contract in this folder
Currently wired API domains include Notification, Inventory, Fulfillment, Account, Feed, and Media.
## Generate Low-Level Clients
The project uses a dedicated code generation environment because the main runtime is currently on Python 3.14 while the model generator still targets earlier Python versions.

View file

@ -7,6 +7,7 @@ from ebay_client.core.http.transport import ApiTransport
from ebay_client.feed.client import FeedClient
from ebay_client.fulfillment.client import FulfillmentClient
from ebay_client.inventory.client import InventoryClient
from ebay_client.media.client import MediaClient
from ebay_client.notification.client import NotificationClient
@ -31,3 +32,4 @@ class EbayClient:
self.fulfillment = FulfillmentClient(transport)
self.account = AccountClient(transport)
self.feed = FeedClient(transport)
self.media = MediaClient(transport)

View file

@ -36,6 +36,7 @@ class ApiTransport:
json_body: Any | None = None,
headers: Mapping[str, str] | None = None,
content: bytes | None = None,
files: Any | None = None,
) -> httpx.Response:
token = self.oauth_client.get_valid_token(scopes=scopes, scope_options=scope_options)
request_headers = dict(self.default_headers)
@ -54,6 +55,7 @@ class ApiTransport:
json=json_body,
headers=request_headers,
content=content,
files=files,
)
except httpx.HTTPError as exc:
raise TransportError(f"HTTP request failed for {method} {path}") from exc

View file

@ -0,0 +1,3 @@
"""Generated Pydantic models from the OpenAPI contract."""
from .models import *

View file

@ -0,0 +1,281 @@
# generated by datamodel-codegen:
# filename: commerce_media_v1_beta_oas3.yaml
# timestamp: 2026-04-07T08:09:11+00:00
from __future__ import annotations
from typing import List, Optional
from pydantic import BaseModel, Field
class CreateDocumentFromUrlRequest(BaseModel):
"""
This type contains the metadata used to create the document ID when creating a document using a URL.
"""
documentType: Optional[str] = Field(
None,
description="The type of the document being created. For example, a <code>USER_GUIDE_OR_MANUAL</code> or a <code>SAFETY_DATA_SHEET</code>. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/media/types/api:DocumentTypeEnum'>eBay API documentation</a>",
)
documentUrl: Optional[str] = Field(
None,
description="The URL of the document being created.<br><br>The document referenced by the URL must be a .pdf, .png, .jpg, or .jpeg file, and must be no larger than 10 MB.",
)
languages: Optional[List[str]] = Field(
None, description="This array shows the language(s) used in the document."
)
class CreateDocumentRequest(BaseModel):
"""
This type contains the metadata used to create the document ID.
"""
documentType: Optional[str] = Field(
None,
description="The type of the document being uploaded. For example, a <code>USER_GUIDE_OR_MANUAL</code> or a <code>SAFETY_DATA_SHEET</code>. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/media/types/api:DocumentTypeEnum'>eBay API documentation</a>",
)
languages: Optional[List[str]] = Field(
None, description="This array shows the language(s) used in the document."
)
class CreateDocumentResponse(BaseModel):
"""
This type provides information about the created document ID.
"""
documentId: Optional[str] = Field(
None,
description='The unique identifier of the document to be uploaded.<br><br>This value is returned in the response and <b>location</b> header of the <b>createDocument</b> and <b>createDocumentFromUrl</b> methods. This ID can be used with the <b>getDocument</b> and <b>uploadDocument</b> methods, and to add an uploaded document to a listing. See <a href="/api-docs/sell/static/inventory/managing-document-media.html#add-documents" target="_blank">Adding documents to listings</a> for more information. ',
)
documentStatus: Optional[str] = Field(
None,
description="The status of the document resource.<br><br>For example, the value <code>PENDING_UPLOAD</code> is the initial state when the reference to the document has been created using the <b>createDocument</b> method. When creating a document using the <b>createDocumentFromUrl</b> method, the initial state will be <code>SUBMITTED</code>. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/media/types/api:DocumentStatusEnum'>eBay API documentation</a>",
)
documentType: Optional[str] = Field(
None,
description="The type of the document uploaded. For example, <code>USER_GUIDE_OR_MANUAL</code>. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/media/types/api:DocumentTypeEnum'>eBay API documentation</a>",
)
languages: Optional[List[str]] = Field(
None, description="This array shows the language(s) used in the document."
)
class CreateImageFromUrlRequest(BaseModel):
"""
A type that provides the location of the image.
"""
imageUrl: Optional[str] = Field(
None,
description='The image URL of the self-hosted picture to upload to eBay Picture Services (EPS). In addition to the picture requirements in <a href="https://www.ebay.com/help/policies/listing-policies/picture-policy?id=4370" target="_blank">Picture policy</a>, the provided URL must be secured using HTTPS (HTTP is not permitted). For more information, see <a href="/api-docs/sell/static/inventory/managing-image-media.html#image-requirements" target="_blank">Image requirements</a>.',
)
class CreateVideoRequest(BaseModel):
"""
The request to create a video, which must contain the video's <b>title</b>, <b>size</b>, and <b>classification</b>. <b>Description</b> is an optional field when creating videos.
"""
classification: Optional[List[str]] = Field(
None,
description="The intended use for this video content. Currently, videos can only be added and associated with eBay listings, so the only supported value is <code>ITEM</code>.",
)
description: Optional[str] = Field(
None, description="The description of the video."
)
size: Optional[int] = Field(
None,
description="The size, in bytes, of the video content. <br><br><b>Max:</b> 157,286,400 bytes",
)
title: Optional[str] = Field(None, description="The title of the video.")
class DocumentMetadata(BaseModel):
"""
This type provides information about the <b>documentId</b>.
"""
fileName: Optional[str] = Field(
None,
description="The name of the file including its extension (for example, <code>drone_user_warranty.pdf</code>).",
)
fileSize: Optional[str] = Field(
None, description="The size, in bytes, of the document content."
)
fileType: Optional[str] = Field(
None,
description="The type of the file uploaded. Supported file types include the following: <code>pdf</code>, <code>jpeg</code>, <code>jpg</code>, and <code>png</code>.",
)
class DocumentResponse(BaseModel):
"""
This type provides information returned about a created document ID, which may or may not have been uploaded.
"""
documentId: Optional[str] = Field(
None, description="The unique ID of the document."
)
documentMetadata: Optional[DocumentMetadata] = Field(
None,
description="This container provides the name, size, and type of the specified file.",
)
documentStatus: Optional[str] = Field(
None,
description="The status of the document resource.<br><br>Once a document has been uploaded using the <b>uploadDocument</b> method, the <b>documentStatus</b> will be <code>SUBMITTED</code>. The document will then either be accepted or rejected. Only documents with the status of <code>ACCEPTED</code> are available to be added to a listing. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/media/types/api:DocumentStatusEnum'>eBay API documentation</a>",
)
documentType: Optional[str] = Field(
None,
description="The type of the document uploaded. For example, <code>USER_GUIDE_OR_MANUAL</code>. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/media/types/api:DocumentTypeEnum'>eBay API documentation</a>",
)
languages: Optional[List[str]] = Field(
None, description="This array shows the language(s) used in the document."
)
class ErrorParameter(BaseModel):
name: Optional[str] = Field(None, description="The object of the error.")
value: Optional[str] = Field(None, description="The value of the object.")
class Image(BaseModel):
"""
A type that provides the location of the image.
"""
imageUrl: Optional[str] = Field(
None, description="The URL of the image's location."
)
class ImageResponse(BaseModel):
"""
A type that provides an image's details including its URL and expiration.
"""
expirationDate: Optional[str] = Field(
None,
description="The date and time when an unused EPS image will expire and be removed from the EPS server, in Coordinated Universal Time (UTC). As long as an EPS image is being used in an active listing, that image will remain on the EPS server and be accessible.",
)
imageUrl: Optional[str] = Field(
None,
description="The EPS URL to access the uploaded image. This URL will be used in listing calls to add the image to a listing.",
)
maxDimensionImageUrl: Optional[str] = Field(
None,
description="The EPS URL to access the maximum dimension version of the uploaded image.",
)
class InputStream(BaseModel):
"""
The streaming input of the video source. The input source must be an .mp4 file of the type MPEG-4 Part 10 or Advanced Video Coding (MPEG-4 AVC).
"""
class Moderation(BaseModel):
"""
A container that provides video moderation information when calling the <strong>getVideo</strong> method.<br /><br />This container is returned if the specified video has been blocked by moderators.<br /><br /><span class="tablenote"><span style="color:#478415"><strong>Tip:</strong></span> See <a href="https://www.ebay.com/help/selling/listings/creating-managing-listings/add-video-to-listing?id=5272#section2" target="_blank">Video moderation and restrictions</a> in the eBay Seller Center for details about video moderation.</span>
"""
rejectReasons: Optional[List[str]] = Field(
None,
description="The reason(s) why the specified video was blocked by moderators.",
)
class Play(BaseModel):
"""
The two streaming video URLs available for a successfully uploaded video with a status of <code>LIVE</code>. The supported streaming video protocols are DASH (Dynamic Adaptive Streaming over HTTP) and HLS (HTTP Live Streaming).
"""
playUrl: Optional[str] = Field(None, description="The playable URL for this video.")
protocol: Optional[str] = Field(
None,
description="The protocol for the video playlist. Supported protocols are DASH (Dynamic Adaptive Streaming over HTTP) and HLS (HTTP\xa0Live Streaming). For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/media/types/api:ProtocolEnum'>eBay API documentation</a>",
)
class Video(BaseModel):
"""
A response field that retrieves all the metadata for the video, including its <b>title</b>, <b>classification</b>, <b>size</b>, <b>description</b>, <b>status</b>, <b>status message</b> (if any), and <b>expiration date</b>.
"""
classification: Optional[List[str]] = Field(
None,
description="The intended use for this video content. Currently, videos can only be added and associated with eBay listings, so the only supported value is <code>ITEM</code>.",
)
description: Optional[str] = Field(
None,
description='The description of the video. The video description is an optional field that can be set using the <a href=" /api-docs/commerce/media/resources/video/methods/createVideo" target="_blank">createVideo</a> method.',
)
expirationDate: Optional[str] = Field(
None,
description="The date and time when an unused video will expire and be removed from the eBay Video Services server, in Coordinated Universal Time (UTC).<br><br>As long as a video is being used in an active listing, that video will remain on the server and be accessible. If a video is not being used on an active listing, its expiration date is automatically set to 30 days after the video's initial upload.",
)
moderation: Optional[Moderation] = Field(
None,
description='The video moderation information that is returned if a video is blocked by moderators.<br /><br /><span class="tablenote"><span style="color:#478415"><strong>Tip:</strong></span> See <a href="https://www.ebay.com/help/selling/listings/creating-managing-listings/add-video-to-listing?id=5272#section2" target="_blank">Video moderation and restrictions</a> in the eBay Seller Center for details about video moderation.</span><br /><br />If the video status is <code>BLOCKED</code>, ensure that the video complies with eBay\'s video formatting and content guidelines. Afterwards, begin the video creation and upload procedure anew using the <strong>createVideo</strong> and <strong>uploadVideo</strong> methods.',
)
playLists: Optional[List[Play]] = Field(
None,
description="The playlist created for the uploaded video, which provides the streaming video URLs to play the video. The supported streaming video protocols are DASH (Dynamic Adaptive Streaming over HTTP) and HLS (HTTP\xa0Live Streaming). The playlist will only be generated if a video is successfully uploaded with a status of <code>LIVE</code>.",
)
size: Optional[int] = Field(
None, description="The size, in bytes, of the video content."
)
status: Optional[str] = Field(
None,
description="The status of the current video resource. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/media/types/api:VideoStatusEnum'>eBay API documentation</a>",
)
statusMessage: Optional[str] = Field(
None,
description="The <b>statusMessage</b> field contains additional information on the status. For example, information on why processing might have failed or if the video was blocked.",
)
thumbnail: Optional[Image] = Field(
None,
description="The URL of the thumbnail image of the video. The thumbnail image's URL must be an eBayPictureURL (EPS URL).",
)
title: Optional[str] = Field(None, description="The title of the video.")
videoId: Optional[str] = Field(None, description="The unique ID of the video.")
class Error(BaseModel):
"""
This type defines the fields that can be returned in an error.
"""
category: Optional[str] = Field(None, description="Identifies the type of erro.")
domain: Optional[str] = Field(
None,
description="Name for the primary system where the error occurred. This is relevant for application errors.",
)
errorId: Optional[int] = Field(
None, description="A unique number to identify the error."
)
inputRefIds: Optional[List[str]] = Field(
None,
description="An array of request elements most closely associated to the error.",
)
longMessage: Optional[str] = Field(
None, description="A more detailed explanation of the error."
)
message: Optional[str] = Field(
None,
description="Information on how to correct the problem, in the end user's terms and language where applicable.",
)
outputRefIds: Optional[List[str]] = Field(
None,
description="An array of request elements most closely associated to the error.",
)
parameters: Optional[List[ErrorParameter]] = Field(
None,
description="An array of name/value pairs that describe details the error condition. These are useful when multiple errors are returned.",
)
subdomain: Optional[str] = Field(
None,
description="Further helps indicate which subsystem the error is coming from. System subcategories include: Initialization, Serialization, Security, Monitoring, Rate Limiting, etc.",
)

View file

@ -0,0 +1,3 @@
from ebay_client.media.client import CreatedMediaResource, MediaClient
__all__ = ["CreatedMediaResource", "MediaClient"]

143
ebay_client/media/client.py Normal file
View file

@ -0,0 +1,143 @@
from __future__ import annotations
from pydantic import BaseModel
from ebay_client.core.http.transport import ApiTransport
from ebay_client.generated.media.models import (
CreateDocumentFromUrlRequest,
CreateDocumentRequest,
CreateDocumentResponse,
CreateImageFromUrlRequest,
CreateVideoRequest,
DocumentResponse,
ImageResponse,
Video,
)
MEDIA_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.inventory"
class CreatedMediaResource(BaseModel):
location: str | None = None
class MediaClient:
def __init__(self, transport: ApiTransport) -> None:
self.transport = transport
def create_image_from_file(
self,
*,
file_name: str,
content: bytes,
content_type: str = "application/octet-stream",
) -> ImageResponse:
return self.transport.request_model(
ImageResponse,
"POST",
"/commerce/media/v1_beta/image/create_image_from_file",
scopes=[MEDIA_SCOPE],
files={"image": (file_name, content, content_type)},
)
def create_image_from_url(self, payload: CreateImageFromUrlRequest) -> ImageResponse:
return self.transport.request_model(
ImageResponse,
"POST",
"/commerce/media/v1_beta/image/create_image_from_url",
scopes=[MEDIA_SCOPE],
headers={"Content-Type": "application/json"},
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
def get_image(self, image_id: str) -> ImageResponse:
return self.transport.request_model(
ImageResponse,
"GET",
f"/commerce/media/v1_beta/image/{image_id}",
scopes=[MEDIA_SCOPE],
)
def create_video(self, payload: CreateVideoRequest) -> CreatedMediaResource:
response = self.transport.request(
"POST",
"/commerce/media/v1_beta/video",
scopes=[MEDIA_SCOPE],
headers={"Content-Type": "application/json"},
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
return CreatedMediaResource(location=response.headers.get("Location"))
def get_video(self, video_id: str) -> Video:
return self.transport.request_model(
Video,
"GET",
f"/commerce/media/v1_beta/video/{video_id}",
scopes=[MEDIA_SCOPE],
)
def upload_video(
self,
video_id: str,
*,
content: bytes,
content_length: int | None = None,
content_range: str | None = None,
) -> None:
headers = {"Content-Type": "application/octet-stream"}
if content_length is not None:
headers["Content-Length"] = str(content_length)
if content_range is not None:
headers["Content-Range"] = content_range
self.transport.request_json(
"POST",
f"/commerce/media/v1_beta/video/{video_id}/upload",
scopes=[MEDIA_SCOPE],
headers=headers,
content=content,
)
def create_document(self, payload: CreateDocumentRequest) -> CreateDocumentResponse:
return self.transport.request_model(
CreateDocumentResponse,
"POST",
"/commerce/media/v1_beta/document",
scopes=[MEDIA_SCOPE],
headers={"Content-Type": "application/json"},
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
def create_document_from_url(self, payload: CreateDocumentFromUrlRequest) -> CreateDocumentResponse:
return self.transport.request_model(
CreateDocumentResponse,
"POST",
"/commerce/media/v1_beta/document/create_document_from_url",
scopes=[MEDIA_SCOPE],
headers={"Content-Type": "application/json"},
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
def get_document(self, document_id: str) -> DocumentResponse:
return self.transport.request_model(
DocumentResponse,
"GET",
f"/commerce/media/v1_beta/document/{document_id}",
scopes=[MEDIA_SCOPE],
)
def upload_document(
self,
document_id: str,
*,
file_name: str,
content: bytes,
content_type: str = "application/octet-stream",
) -> DocumentResponse:
return self.transport.request_model(
DocumentResponse,
"POST",
f"/commerce/media/v1_beta/document/{document_id}/upload",
scopes=[MEDIA_SCOPE],
files={"file": (file_name, content, content_type)},
)

View file

@ -20,6 +20,11 @@ class ApiSpec:
API_SPECS = {
"media": ApiSpec(
name="media",
spec_path=ROOT / "commerce_media_v1_beta_oas3.yaml",
output_path=GENERATED_ROOT / "media",
),
"notification": ApiSpec(
name="notification",
spec_path=ROOT / "commerce_notification_v1_oas3.yaml",
@ -90,12 +95,24 @@ def run_generation(spec: ApiSpec, *, fail_on_warning: bool) -> None:
command.append("--disable-warnings")
subprocess.run(command, check=True, cwd=str(ROOT))
normalize_generated_module(spec.output_path / "models.py")
(spec.output_path / "__init__.py").write_text(
'"""Generated Pydantic models from the OpenAPI contract."""\n\nfrom .models import *\n',
encoding="utf-8",
)
def normalize_generated_module(file_path: Path) -> None:
raw_bytes = file_path.read_bytes()
try:
content = raw_bytes.decode("utf-8")
except UnicodeDecodeError:
content = raw_bytes.decode("cp1252")
content = content.replace("\u00a0", " ")
file_path.write_text(content, encoding="utf-8")
def main() -> int:
args = parse_args()
specs = [API_SPECS[args.api]] if args.api else [API_SPECS[name] for name in sorted(API_SPECS)]

View file

@ -13,6 +13,15 @@ from ebay_client.generated.account.models import Programs
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.media.models import (
CreateDocumentFromUrlRequest,
CreateDocumentRequest,
CreateImageFromUrlRequest,
CreateVideoRequest,
DocumentResponse,
ImageResponse,
Video,
)
from ebay_client.generated.notification.models import (
Config,
CreateSubscriptionFilterRequest,
@ -26,6 +35,7 @@ from ebay_client.generated.notification.models import (
UpdateSubscriptionRequest,
)
from ebay_client.inventory.client import InventoryClient
from ebay_client.media.client import CreatedMediaResource, MediaClient
from ebay_client.notification.client import NotificationClient
@ -359,4 +369,155 @@ def test_feed_wrapper_accepts_any_documented_feed_scope_option(httpx_mock: HTTPX
["https://api.ebay.com/oauth/api_scope/sell.marketing"],
["https://api.ebay.com/oauth/api_scope/commerce.catalog.readonly"],
["https://api.ebay.com/oauth/api_scope/sell.analytics.readonly"],
]
]
def test_media_wrapper_returns_image_model_from_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/commerce/media/v1_beta/image/create_image_from_url",
json={"imageUrl": "https://i.ebayimg.com/images/g/demo.jpg", "expirationDate": "2026-12-31T00:00:00Z"},
status_code=201,
)
client = MediaClient(build_transport())
result = client.create_image_from_url(
CreateImageFromUrlRequest(imageUrl="https://example.test/demo.jpg")
)
assert isinstance(result, ImageResponse)
assert result.imageUrl == "https://i.ebayimg.com/images/g/demo.jpg"
request = httpx_mock.get_requests()[0]
body = json.loads(request.content.decode("utf-8"))
assert body["imageUrl"] == "https://example.test/demo.jpg"
def test_media_wrapper_serializes_multipart_image_upload(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/commerce/media/v1_beta/image/create_image_from_file",
json={"imageUrl": "https://i.ebayimg.com/images/g/uploaded.jpg"},
status_code=201,
)
client = MediaClient(build_transport())
result = client.create_image_from_file(
file_name="demo.jpg",
content=b"binary-image-content",
content_type="image/jpeg",
)
assert isinstance(result, ImageResponse)
request = httpx_mock.get_requests()[0]
assert request.headers["Content-Type"].startswith("multipart/form-data;")
assert b"name=\"image\"" in request.content
assert b"filename=\"demo.jpg\"" in request.content
assert b"binary-image-content" in request.content
def test_media_wrapper_returns_created_resource_location_for_video(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/commerce/media/v1_beta/video",
status_code=201,
headers={"Location": "https://api.ebay.com/commerce/media/v1_beta/video/VIDEO-1"},
)
client = MediaClient(build_transport())
result = client.create_video(
CreateVideoRequest(title="Demo", size=1024, classification=["ITEM"])
)
assert isinstance(result, CreatedMediaResource)
assert result.location == "https://api.ebay.com/commerce/media/v1_beta/video/VIDEO-1"
def test_media_wrapper_returns_video_model(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="GET",
url="https://api.ebay.com/commerce/media/v1_beta/video/VIDEO-1",
json={"videoId": "VIDEO-1", "status": "LIVE", "title": "Demo"},
)
client = MediaClient(build_transport())
result = client.get_video("VIDEO-1")
assert isinstance(result, Video)
assert result.videoId == "VIDEO-1"
def test_media_wrapper_uploads_video_bytes(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/commerce/media/v1_beta/video/VIDEO-1/upload",
status_code=200,
)
client = MediaClient(build_transport())
client.upload_video(
"VIDEO-1",
content=b"video-bytes",
content_length=11,
content_range="bytes 0-10/11",
)
request = httpx_mock.get_requests()[0]
assert request.headers["Content-Type"] == "application/octet-stream"
assert request.headers["Content-Length"] == "11"
assert request.headers["Content-Range"] == "bytes 0-10/11"
assert request.content == b"video-bytes"
def test_media_wrapper_returns_document_models(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/commerce/media/v1_beta/document",
json={"documentId": "DOC-1", "documentStatus": "PENDING_UPLOAD"},
status_code=201,
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/commerce/media/v1_beta/document/create_document_from_url",
json={"documentId": "DOC-2", "documentStatus": "SUBMITTED"},
status_code=201,
)
httpx_mock.add_response(
method="GET",
url="https://api.ebay.com/commerce/media/v1_beta/document/DOC-1",
json={"documentId": "DOC-1", "documentStatus": "ACCEPTED", "documentType": "USER_GUIDE_OR_MANUAL"},
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/commerce/media/v1_beta/document/DOC-1/upload",
json={"documentId": "DOC-1", "documentStatus": "SUBMITTED"},
status_code=200,
)
client = MediaClient(build_transport())
created = client.create_document(
CreateDocumentRequest(documentType="USER_GUIDE_OR_MANUAL", languages=["en-US"])
)
created_from_url = client.create_document_from_url(
CreateDocumentFromUrlRequest(
documentType="USER_GUIDE_OR_MANUAL",
documentUrl="https://example.test/guide.pdf",
languages=["en-US"],
)
)
fetched = client.get_document("DOC-1")
uploaded = client.upload_document(
"DOC-1",
file_name="guide.pdf",
content=b"%PDF-1.7",
content_type="application/pdf",
)
assert created.documentId == "DOC-1"
assert created_from_url.documentId == "DOC-2"
assert isinstance(fetched, DocumentResponse)
assert fetched.documentStatus == "ACCEPTED"
assert uploaded.documentStatus == "SUBMITTED"
upload_request = httpx_mock.get_requests()[3]
assert upload_request.headers["Content-Type"].startswith("multipart/form-data;")
assert b"filename=\"guide.pdf\"" in upload_request.content
assert b"%PDF-1.7" in upload_request.content